Posted on 2007-07-18 13:12
Matthew Chen 閱讀(222)
評論(0) 編輯 收藏 所屬分類:
Java MultiThread
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"); |
public static String transName; |
public static double amount; |
class TransThread extends Thread |
TransThread (FinTrans ft, String name) |
super (name); //保存線程的名稱 Save thread's 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); |
仔細看看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 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"); |
private String transName; |
synchronized void update (String transName, double amount) |
this.transName = transName; |
System.out.println (this.transName + " " + this.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.update ("Deposit", 2000.0); |
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. 同步類方法能夠調用一個對象的同步方法或使用對象去鎖住一個同步塊。在那種情形下,一個線程最初獲得同步類方法的類鎖并且接下來獲得對象的對象鎖。因此,調用同步實例方法的一個同步類方法也獲得兩個鎖。 |
//剛好在執行進入instanceMethod()前獲得對象鎖 |
synchronized void instanceMethod () |
//當線程離開instanceMethod()時釋放對象鎖 |
//剛好在執行進入classMethod()前獲得類鎖 |
synchronized static void classMethod (LockTypes lt) |
//當線程離開classMethod()時釋放類鎖 |
代碼段示范了調用同步實例方法instanceMethod()的同步類方法classMethod()。通過閱讀注解,你看到classMethod()首先獲得它的類鎖接下來獲得與lt引用的LockTypes對象相關的對象鎖。 |
警告:不要同步一個線程對象的run()方法因為多線程需要執行run()。因為那些線程企圖對同一個對象同步,所以在一個時間里只有一個線程能夠執行run()。結果,在每一個線程能訪問run()前必須等待前一線程結束。 |