Posted on 2007-07-18 13:11
Matthew Chen 閱讀(242)
評論(0) 編輯 收藏 所屬分類:
Java MultiThread
開發者有時創建的多線程程序會生成錯誤值或產生其它奇怪的行為。古怪行為一般出現在一個多線程程序沒使用同步連載線程訪問關鍵代碼部份的時候。同步連載線程訪問關鍵代碼部份是什么意思呢?在這篇文章中解釋了同步,Java的同步機制,以及當開發者沒有正確使用這個機制時出現的兩個問題。一旦你看完這篇文章,你就可以避免在你的多線程Java程序中因缺乏同步而產生的奇怪行為。 |
創建多線程Java程序難嗎?僅從《用Java線程獲取優異性能(I)》中獲得的信息你就可以回答,不。畢竟,我已經向你顯示了如何輕松地創建線程對象,通過調用Thread的start()方法起動與這些對象相關的線程,以及通過調用其它Thread方法,比如三個重載的join()方法執行簡單的線程操作。至今仍有許多開發者在開發一些多線程程序時面臨困難境遇。他們的程序經常功能不穩定或產生錯誤值。例如,一個多線程程序可能將不正確的雇員資料存貯在數據庫中,比如姓名和地址。姓名可能屬于一個雇員的,而地址卻屬于另一個的。是什么引起這種奇怪行為的呢? 是缺乏同步:連載行為,或在同一時間排序,線程訪問那些讓多重線程操作的類和字段變量實例的代碼序列,以及其他共享資源。我稱這些代碼序列為關鍵代碼部份。 |
注意:不象類和實例字段變量,線程不能共享本地變量和參數。原因是:本地變量和參數在一個線程方法中分配——叫堆棧。結果,每一個線程都收到它自己對那些變量的拷貝。相反,線程能夠共享類字段和實例字段因為那些變量在一個線程方法(叫堆棧)中沒有被分配。取而代之,它們作為類(類字段)或對象(實例字段)的一部份在共享內存堆中被分配。 |
這篇文章將教你如何使用同步連載線程訪問關鍵代碼部份。我用一個說明為什么一些多線程程序必須使用同步的例子作為開始。我接下來就監視器和鎖探討Java的同步機制和synchronized 關鍵字。我通過研究由這樣的錯用產生的兩個問題判定常常因為不正確的使用同步機制而否認了它的好處。 |
為什么我們需要同步呢?一種回答,考慮這個例子:你寫一個使用一對線程模擬取款/存款金融事務的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"); |
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); |
NeedForSynchronizationDemo的源代碼有兩個關鍵代碼部份:一個可理解為存款線程,另一個可理解為取款線程。在存款線程關鍵代碼部份中,線程分配Deposit String對象的引用給共享變量transName及分配2000.0 給共享變量amount。同樣,在取款關鍵代碼部份,線程分配Withdrawal String對象的引用給transName及分配250.0給amount。在每個線程的分配之后打印那些變量的內容。當你運行NeedForSynchronizationDemo時,你可能期望輸出類似于Withdrawal 250.0 和Deposit 2000.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位雙精度浮點型變量。一個線程可能在完成第一步后等待其它線程執行所有的兩步。接下來,第一個線程可能醒來并完成第二步,產生一個值既不同于第一個線程也不同于第二線程的值的變量。結果,如果至少一個線程能夠修改一個長整型變量或一個雙精度浮點型變量,那些讀取和(或)修改那個變量的所有線程就必須使用同步連載訪問。 |