摘要
開發人員有時建立的多線程程式會建置錯誤值或產生其它奇怪的行為。古怪行為一般出現在一個多線程程式沒使用同步連載線程訪問關鍵代碼部份的時候。同步連載線程訪問關鍵代碼部份是什麼意思呢?在這篇文章中解釋了同步,Java的同步機制,以及當開發人員沒有正確使用這個機制時出現的兩個問題。一旦你看完這篇文章,你就可以避免在你的多線程Java程式中因缺乏同步而產生的奇怪行為。
建立多線程Java程式難嗎?僅從《用Java線程擷取優異效能(I)》中獲得的資訊你就可以回答,不。畢竟,我已經向你顯示了如何輕鬆地建立線程對象,通過調用Thread的start()方法起動與這些對象相關的線程,以及通過調用其它Thread方法,比如三個重載的join()方法執行簡單的線程操作。至今仍有許多開發人員在開發一些多線程程式時面臨困難境遇。他們的程式經常功能不穩定或產生錯誤值。例如,一個多線程程式可能將不正確的僱員資料存貯在資料庫中,比如姓名和地址。姓名可能屬於一個僱員的,而地址卻屬於另一個的。是什麼引起這種奇怪行為的呢? 是缺乏同步:連載行為,或在同一時間排序,線程訪問那些讓多重線程操作的類和欄位變數執行個體的代碼序列,以及其他共用資源。我稱這些代碼序列為關鍵代碼部份。
注意:不象類和執行個體欄位變數,線程不能共用本地變數和參數。原因是:本地變數和參數在一個線程方法中分配——叫堆棧。結果,每一個線程都收到它自己對那些變數的拷貝。相反,線程能夠共用類欄位和執行個體欄位因為那些變數在一個線程方法(叫堆棧)中沒有被分配。取而代之,它們作為類(類欄位)或對象(執行個體欄位)的一部份在共用記憶體堆中被分配。
這篇文章將教你如何使用同步連載線程訪問關鍵代碼部份。我用一個說明為什麼一些多線程程式必須使用同步的例子作為開始。我接下來就監視器和鎖探討Java的同步機制和synchronized 關鍵字。我通過研究由這樣的錯用產生的兩個問題判定常常因為不正確的使用同步機制而否認了它的好處。
閱讀關於線程程式的整個系列:
· 第I部份:介紹線程、線程類及Runnable
· 第II部份:使用同步連載線程訪問關鍵代碼部份
對於同步的需要
為什麼我們需要同步呢?一種回答,考慮這個例子:你寫一個使用一對線程類比取款/存款金融事務的Java程式。在那個程式中,一個線程處理存款,同時其它線程正處理取款。每一個線程操作一對共用變數、類及執行個體欄位變數,這些用來標識金融事務的姓名和帳號。對於一個正確的金融事務,每一個線程必須在其它線程開始給name和amount賦值前(並且同時列印那些值)給name和amount變數賦值(並列印那些值,類比存貯事務)。其原始碼如下:
列表1. NeedForSynchronizationDemo.java
// NeedForSynchronizationDemo.java
class NeedForSynchronizationDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //儲存線程名稱
this.ft = ft; //儲存對金融事務對象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
//存款線程關鍵代碼部份的開始
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
//存款線程關鍵代碼部份的結束
}
else
{
//取款線程關鍵代碼部份的開始
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
//取款線程關鍵代碼部份的結束
}
}
}
}
NeedForSynchronizationDemo的原始碼有兩個關鍵代碼部份:一個可理解為存款線程,另一個可理解為取款線程。在存款線程關鍵代碼部份中,線程分配Deposit String對象的引用給共用變數transName及分配2000.0 給共用變數amount。同樣,在取款關鍵代碼部份,線程分配Withdrawal String對象的引用給transName及分配250.0給amount。在每個線程的分配之後列印那些變數的內容。當你運行NeedForSynchronizationDemo時,你可能期望輸出類似於Withdrawal 250.0 和Deposit 2000.0兩行組成的列表。相反,你收到的輸出如下所示:
Withdrawal 250.0
Withdrawal 2000.0
Deposit 2000.0
Deposit 2000.0
Deposit 250.0
程式明顯有問題。取款線程不應該類比$2,000的取款,存款線程不應該類比$250的存款。每一個線程產生不一致的輸出。是什麼引起了這些矛盾呢?我們是如下認為的:
· 在一個單一處理器機器上,線程共用處理器。結果,一個線程僅能執行一定時間段。在其它時間裡, JVM/作業系統暫停那個線程的執行並允許其它線程執行——一種線程時序安排。在一個多處理器機器上,依靠線程和處理器的數目,每一個線程都能擁有它自己的處理器。
· 在一單一處理器機器上,一個線程的執行時間段沒有足夠長到在其它線程開始執行的關鍵代碼部份前完成它自己的關鍵代碼部分。在一個多處理器機器上,線程能夠同時執行它們自己的關鍵代碼部份。然而,它們可能在不同的時間進入它們的關鍵代碼部份。
· 無論是單一處理器或是多處理器機器,下面的情形都可能發生:線程A在它的關鍵代碼部份分配一個值給共用變數X並決定執行一個要求100毫秒的輸入/輸出操作。接下來線程B進入它的關鍵代碼部份,分配一個不同的值給X,執行一個50毫秒的輸入/輸出操作並分配值給共用變數Y 和Z。線程A的輸入/輸出操作完成,並分配它自己的值給Y和Z。因為X包含一個B分配的值,然而Y和Z包含A分配的值,這是一個矛盾的結果。
這個矛盾是怎樣在NeedForSynchronizationDemo中產生的呢?假設存款線程執行ft.transName = "Deposit"並且接下來調用Thread.sleep()。在那一點,存款線程交出處理器控制一段時間進行休眠,讓取款線程執行。假定存款線程休眠500毫秒(感謝Math.random()從0到999毫秒範圍隨機選取一個值)。在存款線程休眠期間,取款線程執行ft.transName = "Withdrawal",休眠50毫秒 (取款線程隨機選取休眠值),醒後執行ft.amount = 250.0並執行System.out.println (ft.transName + " " + ft.amount)—所有都在存款線程醒來之前。結果,取款線程列印Withdrawal 250.0,那是正確的。當存款線程醒來執行ft.amount = 2000.0,接下來執行System.out.println (ft.transName + " " + ft.amount)。這個時間Withdrawal 2000.0 列印,那是不正確的。雖然存款線程先前分配"Deposit"的引用給transName,但這個引用隨後會在取款線程分配”Withdrawal”引用給那個共用變數時消失。當存款線程醒來時,它就不能存貯正確的引用到transName,但通過分配2000.0給amount繼續它的執行。雖然兩個變數都不會有無效的值,但它們的結合值卻是矛盾的。假如這樣的話,它們的值顯示企圖取款$2,000。
很久以前,電腦科學家發明了描述導致矛盾的多線程組合行為的一個術語。術語是競態條件(race condition)—每一個線程競相在其它線程進入同一關鍵代碼部份前完成它自己的關鍵代碼部份的行為。作為NeedForSynchronizationDemo示範,線程的執行順序是不可知的。這裡不能保證一個線程能夠在其它線程進入關鍵代碼部份前完成它自己的關鍵代碼部份。因此,我們會有競態條件引起不一致。要阻止競態條件,每一個線程必須在其它線程進入同一關鍵代碼部份或其它操作同一共用變數或資源的相關關鍵代碼部份前完成它自己的關鍵代碼部份。對於一個關鍵代碼部份沒有連載存取方法(即是在一個時間只允許訪問一個線程),你就不能阻止競態條件或不一致的出現。幸運的是,Java提供了連載線程訪問的方法:通過它的同步機制。
注意:對於Java的類型,只有長整型和雙精確度浮點型變數傾向於不一致。為什嗎?一個32位JVM一般用兩個臨近32位步長訪問一個64位的長整型變數或一個64位雙精確度浮點型變數。一個線程可能在完成第一步後等待其它線程執行所有的兩步。接下來,第一個線程可能醒來並完成第二步,產生一個值既不同於第一個線程也不同於第二線程的值的變數。結果,如果至少一個線程能夠修改一個長整型變數或一個雙精確度浮點型變數,那些讀取和(或)修改那個變數的所有線程就必須使用同步連載訪問。
Java的同步機制
Java提供一個同步機制以阻止多於一個的線程在時間的任意一點在一個或多個關鍵代碼部份執行代碼。這種機制將自己建立在監視器和鎖的概念基礎上。一個監視器被作為包在關鍵代碼部份周圍的保護,一個鎖被作為監視器用來防止多重線程進入監視器的一個軟體實體。其想法是:當一個線程想進入一個監視器監視著的關鍵代碼部份時,那個線程必須獲得一個與監視器相關的對象的鎖。(每個對象都有它自己的鎖)如果一些其它線程儲存著這個鎖, JVM會強迫請求線程在一個與監視器/鎖有關的等待地區等待。當監視器中的線程釋放鎖時, JVM從監視器的等待地區中移出等待線程並允許那個線程獲得鎖且處理監視器的關鍵代碼部份。
要和監視器/鎖一起工作, JVM提供了monitorenter和monitorexit 指令。幸運地是,你不需要在如此低層級地工作。取而代之,你能夠在synchronized聲明和同步方法中使用Java的synchronized關鍵字。
同步聲明
一些關鍵代碼部份佔了它們封裝方法的一小部份。為了防止多重線程訪問這們的關鍵代碼部份,你可使用synchronized聲明。這個聲明有如下的文法:
'synchronized' '(' objectidentifier ')'
'{'
//關鍵代碼部份
'}'
synchronized聲明用關鍵字synchronized開始及用一個objectidentifier,這出現在一對圓括弧之間。objectidentifier 引用一個與synchronized 聲明描述的監視器相關的鎖對象。最後,Java聲明的關鍵代碼部份出現在一對花括弧之間。你怎樣解釋synchronized聲明呢?看看如下代碼片斷:
synchronized ("sync object")
{
//訪問共用變數及其它共用資源
}
從一個原始碼觀點看,一個線程企圖進入synchronized聲明保護的關鍵代碼部份。在內部, JVM 檢查是否一些其它線程式控制制著與"sync object"對象相關的鎖。如果沒有其它線程式控制制著鎖, JVM將鎖給請求線程並允許那個線程進入花括弧之間的關鍵代碼部份。然而,如果有其它線程式控制制著鎖, JVM會強迫請求線程在一個私人等待地區等待直到在關鍵代碼部份內的當前線程完成執行最後聲明及經過最後的花括弧。
你能夠使用synchronized聲明去消除NeedForSynchronizationDemo的競態條件。如何消除,請看練習列表2:
列表2. SynchronizationDemo1.java
// SynchronizationDemo1.java
class SynchronizationDemo1
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //儲存線程的名稱 Save thread's name
this.ft = ft; //儲存對金融事務對象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (ft)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
else
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
仔細看看SynchronizationDemo1,run()方法包含兩個夾在synchronized (ft) { and }間的關鍵代碼部份。每個存款和取款線程必須在任一線程進入它的關鍵代碼部份前獲得與ft引用的FinTrans對象相關的鎖。假如如果存款線程在它的關鍵代碼部份且取款線程想進入它自己的關鍵代碼部份,取款線程就應努力獲得鎖。因為當存款線程在它的關鍵代碼部份執行時控制著鎖, JVM 便強迫取款線程等待直到存款線程執行完關鍵代碼部份並釋放鎖。(當執行離開關鍵代碼部份時,鎖自動釋放)
技巧:當你需要決定是否一個線程式控制制與一個給定對象相關的鎖時,調用Thread的靜態布爾holdsLock(Object o)方法。如果線程調用控制著與對象相關的鎖的方法,這個方法便返回一個布爾真值。否則,返回一個假值。例如,如果你打算將System.out.println (Thread.holdsLock (ft))放置在SynchronizationDemo1的main()方法末尾, holdsLock()將返回假值。返回 假值是因為執行main()方法的主線程沒有使用同步機制獲得任何鎖。可是,如果你打算將System.out.println (Thread.holdsLock (ft))放在run()的synchronized (ft)聲明中, holdsLock()將返回真值因為無論是存款線程或是取款線程都不得不在那些線程能夠進入它的關鍵代碼部份前獲得與ft引用的FinTrans對象相關的鎖。
Synchronized方法
你能夠通過你的程式的原始碼使用synchronized聲明。然而,你也可能陷入過多使用這樣的聲明而導致代碼效率低。例如,假設你的程式包含一個帶兩個連續synchronized聲明的方法,每一個聲明都企圖獲得同一公用對象的鎖。因為獲得和翻譯對象的鎖要消耗時間,重複調用(在一個迴圈中)那個方法會降低程式的效能。每次對那個方法的一個調用都必須獲得和釋放兩個鎖。程式花費大量的時間獲得和釋放鎖。要消除這個問題,你應考慮使用同步方法。
一個同步方法不是一個執行個體就是一個其頭包含synchronized關鍵字的類方法。例如: synchronized void print (String s)。當你同步一個完整執行個體方法時,一個線程必須獲得與那個方法調用出現的對象相關的鎖。例如,給一個ft.update("Deposit", 2000.0)執行個體方法調用,並且假定update()是同步的,一個方法必須獲得與ft引用的對象相關的鎖。要看一個SynchronizationDemo1版本的同步方法的原始碼,請查看列表3:
列表3. SynchronizationDemo2.java
// SynchronizationDemo2.java
class SynchronizationDemo2
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
private String transName;
private double amount;
synchronized void update (String transName, double amount)
{
this.transName = transName;
this.amount = amount;
System.out.println (this.transName + " " + this.amount);
}
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //儲存線程名稱
this.ft = ft; //儲存對金融事務對象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
if (getName ().equals ("Deposit Thread"))
ft.update ("Deposit", 2000.0);
else
ft.update ("Withdrawal", 250.0);
}
}
雖然比列表2稍微更簡潔,表3達到的是同一目的。如果存款線程調用update()方法, JVM檢查看是否取款線程已經獲得與ft引用的對象相關的鎖。如果是這樣,存款線程就等待。否則,那個線程就進入關鍵代碼部份。
SynchronizationDemo2示範了一個同步執行個體方法。然而,你也能夠同步class 方法。例如, java.util.Calendar類聲明了一個public static synchronized Locale [] getAvailableLocales() 方法。因為類方法沒有一個this引用的概念,那麼類方法從哪裡獲得它的鎖呢?類方法從類對象獲得它們的鎖——每一個與Class對象相關的載入的類,從那些載入的類的類方法得到它們的鎖。我稱這樣的鎖為class locks。
一些程式混淆同步執行個體方法和同步類方法。為協助你理解在同步類方法調用同步執行個體方法的程式中到底發生了什麼,應在頭腦裡保持如下兩個觀點:
1. 對象鎖和類鎖互相沒有關係。它們是不同的實體。你獨立地獲得和釋放每一個鎖。一個調用同步類方法的同步執行個體方法獲得兩個鎖。首先,同步執行個體方法獲得它的對象的對象鎖。其次,那個方法獲得同步類方法的類鎖。
2. 同步類方法能夠調用一個對象的同步方法或使用對象去鎖住一個同步塊。在那種情形下,一個線程最初獲得同步類方法的類鎖並且接下來獲得對象的對象鎖。因此,調用同步執行個體方法的一個同步類方法也獲得兩個鎖。
下面的代碼片斷描述了這兩個觀點:
class LockTypes
{
//剛好在執行進入instanceMethod()前獲得對象鎖
synchronized void instanceMethod ()
{
//當線程離開instanceMethod()時釋放對象鎖
}
//剛好在執行進入classMethod()前獲得類鎖
synchronized static void classMethod (LockTypes lt)
{
lt.instanceMethod ();
//剛好在關鍵代碼部份執行前獲得對象鎖
synchronized (lt)
{
//關鍵代碼部份
//當線程離開關鍵代碼部份時釋放對象鎖
}
//當線程離開classMethod()時釋放類鎖
}
}
程式碼片段示範了調用同步執行個體方法instanceMethod()的同步類方法classMethod()。通過閱讀註解,你看到classMethod()首先獲得它的類鎖接下來獲得與lt引用的LockTypes對象相關的對象鎖。
警告:不要同步一個線程對象的run()方法因為多線程需要執行run()。因為那些線程企圖對同一個對象同步,所以在一個時間裡只有一個線程能夠執行run()。結果,在每一個線程能訪問run()前必須等待前一線程結束。
同步機制的兩個問題
儘管其簡單,開發人員經常濫用Java的同步機制會導致程式由不同步變得死結。這章將檢查這些問題並提供一對避免它們的建議。
注意:一個與同步機制有關的線程問題是與鎖的獲得和釋放有關的時間成本。換句話說,一個線程將花費時間去獲得或釋放一個鎖。當在一個迴圈中獲得/釋放一個鎖,單獨的時間成本合計起來就會降低效能。對於舊的JVMs,鎖的獲得時間成本經常導致重大的效能損失。幸運地是, Sun微系統的HotSpot JVM (其裝載在J2SE SDK上)提供快速的鎖的獲得和釋放,大大減少了對這些程式的影響。
不同步
在一個線程自動或不自動(通過一個例外)退出一個關鍵代碼部份時,它釋放一個鎖以便另一個線程能夠得以進入。假設兩個線程想進入同一個關鍵代碼部份,為了阻止兩個線程同時進入那個關鍵代碼部份,每個線程必須努力獲得同一個鎖。如果每一個線程企圖獲得一個不同的鎖並成功了,兩個線程都進入了關鍵代碼部份,則兩個線程都不得不等待其它線程釋放它的鎖因為其它線程獲得了一個不同的鎖。最終結果是:沒有同步。示範如列表4:
列表4. NoSynchronizationDemo.java
// NoSynchronizationDemo.java
class NoSynchronizationDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //儲存線程的名稱
this.ft = ft; //儲存對金融事務對象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (this)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
else
{
synchronized (this)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
當你運行NoSynchronizationDemo時,你將看到類似如下的輸出:
Withdrawal 250.0
Withdrawal 2000.0
Deposit 250.0
Withdrawal 2000.0
Deposit 2000.0
儘管使用了synchronized聲明,但沒有同步發生。為什嗎?檢查synchronized (this)。因為關鍵字this指向當前對象,存款線程企圖獲得與初始化分配給tt1的TransThread對象引用有關的鎖。 (在main()方法中)。類似的,取款線程企圖獲得與初始化分配給tt2的TransThread對象引用有關的鎖。我們有兩個不同的TransThread對象,並且每一個線程企圖在進入它自己關鍵代碼部份前獲得與其各自TransThread對象相關的鎖。因為線程獲得不同的鎖,兩個線程都能在同一時間進入它們自己的關鍵代碼部份。結果是沒有同步。
技巧:為了避免一個沒有同步的情形,選擇一個對於所有相關線程都公有的對象。那樣的話,這些線程競相獲得同一個對象的鎖,並且同一時間僅有一個線程在能夠進入相關的關鍵代碼部份。
死結
在有些程式中,下面的情形可能出現:線上程B能夠進入B的關鍵代碼部份前線程A獲得一個線程B需要的鎖。類似的,線上程A能夠進入A的關鍵代碼部份前線程B獲得一個線程A需要的鎖。因為兩個線程都沒有擁有它自己需要的鎖,每個線程都必須等待獲得它的鎖。此外,因為沒有線程能夠執行,沒有線程能夠釋放其它線程的鎖,並且程式執行被凍結。這種行為叫作死結(deadlock)。其示範列如表5:
列表5. DeadlockDemo.java
// DeadlockDemo.java
class DeadlockDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
private static String anotherSharedLock = "";
TransThread (FinTrans ft, String name)
{
super (name); //儲存線程的名稱
this.ft = ft; //儲存對金融事務對象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (ft)
{
synchronized (anotherSharedLock)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
else
{
synchronized (anotherSharedLock)
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
}
如果你運行DeadlockDemo,你將可能看到在應用程式凍結前僅一個單獨輸出行。要解凍DeadlockDemo,按Ctrl-C (假如你正在一個Windows命令提示字元中使用Sun的SDK1.4)。
什麼將引起死結呢?仔細查看原始碼。存款線程必須在它能夠進入其內部關鍵代碼部份前獲得兩個鎖。與ft引用的FinTrans對象有關的外部鎖和與anotherSharedLock引用的String對象有關的內部鎖。類似的,取款線程必須在其能夠進入它自己的內部關鍵代碼部份前獲得兩個鎖。與anotherSharedLock引用的String對象有關的外部鎖和與ft引用的FinTrans對象有關的內部鎖。假定兩個線程的執行命令是每個線程獲得它的外部鎖。因此,存款線程獲得它的FinTrans鎖,以及取款線程獲得它的String鎖。現在兩個線程都執行它們的外部鎖,它們處在它們相應的外部關鍵代碼部份。兩個線程接下來企圖獲得內部鎖,因此它們能夠進入相應的內部關鍵代碼部份。
存款線程企圖獲得與anotherSharedLock引用對象相關的鎖。然而,因為取款線程式控制制著鎖所以存款線程必須等待。類似的,取款線程企圖獲得與ft引用對象相關的鎖。但是取款線程不能獲得那個鎖因為存款線程(它正在等待)控制著它。因此,取款線程也必須等待。兩個線程都不能操作因為兩個線程都不能釋放它控制著的鎖。兩個線程不能釋放它控制著的鎖是因為每個線程都正在等待。每個線程都死結,並且程式凍結。
技巧:為了避免死結,仔細分析你的原始碼看看當一個同步方法調用其它同步方法時什麼地方可能出現線程互相企圖獲得彼此的鎖。你必須這樣做因為JVM不能探測並防止死結。
回顧
為了使用線程達到優異效能,你將遇到你的多線程程式需要連載訪問關鍵代碼部份的情形。同步可以有效地阻止在奇怪程式行為中產生的不一致。你能夠使用synchronized聲明以保護一個方法的部份,或同步整個方法。但應仔細檢查你的代碼以防止可能造成同步失敗或死結的故障。