Posted on 2007-07-18 13:13
Matthew Chen 閱讀(212)
評論(0) 編輯 收藏 所屬分類:
Java MultiThread
盡管其簡單,開發者經常濫用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"); |
public static String transName; |
public static double amount; |
class TransThread extends Thread |
TransThread (FinTrans ft, String name) |
this.ft = ft; //保存對金融事務對象的引用 |
for (int i = 0; i < 100; i++) |
if (getName ().equals ("Deposit Thread")) |
ft.transName = "Deposit"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
System.out.println (ft.transName + " " + ft.amount); |
ft.transName = "Withdrawal"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
System.out.println (ft.transName + " " + ft.amount); |
當你運行NoSynchronizationDemo時,你將看到類似如下的輸出: |
盡管使用了synchronized聲明,但沒有同步發生。為什么?檢查synchronized (this)。因為關鍵字this指向當前對象,存款線程企圖獲得與初始化分配給tt1的TransThread對象引用有關的鎖。 (在main()方法中)。類似的,取款線程企圖獲得與初始化分配給tt2的TransThread對象引用有關的鎖。我們有兩個不同的TransThread對象,并且每一個線程企圖在進入它自己關鍵代碼部份前獲得與其各自TransThread對象相關的鎖。因為線程獲得不同的鎖,兩個線程都能在同一時間進入它們自己的關鍵代碼部份。結果是沒有同步。 |
技巧:為了避免一個沒有同步的情形,選擇一個對于所有相關線程都公有的對象。那樣的話,這些線程競相獲得同一個對象的鎖,并且同一時間僅有一個線程在能夠進入相關的關鍵代碼部份。 |
在有些程序中,下面的情形可能出現:在線程B能夠進入B的關鍵代碼部份前線程A獲得一個線程B需要的鎖。類似的,在線程A能夠進入A的關鍵代碼部份前線程B獲得一個線程A需要的鎖。因為兩個線程都沒有擁有它自己需要的鎖,每個線程都必須等待獲得它的鎖。此外,因為沒有線程能夠執行,沒有線程能夠釋放其它線程的鎖,并且程序執行被凍結。這種行為叫作死鎖(deadlock)。其示范列如表5: |
public static void main (String [] args) |
FinTrans ft = new FinTrans (); |
TransThread tt1 = new TransThread (ft, "Deposit Thread"); |
TransThread tt2 = new TransThread (ft, "Withdrawal Thread"); |
public static String transName; |
public static double amount; |
class TransThread extends Thread |
private static String anotherSharedLock = ""; |
TransThread (FinTrans ft, String name) |
this.ft = ft; //保存對金融事務對象的引用 |
for (int i = 0; i < 100; i++) |
if (getName ().equals ("Deposit Thread")) |
synchronized (anotherSharedLock) |
ft.transName = "Deposit"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
System.out.println (ft.transName + " " + ft.amount); |
synchronized (anotherSharedLock) |
ft.transName = "Withdrawal"; |
Thread.sleep ((int) (Math.random () * 1000)); |
catch (InterruptedException e) |
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聲明以保護一個方法的部份,或同步整個方法。但應仔細檢查你的代碼以防止可能造成同步失敗或死鎖的故障。 |