JAVA並發設計模式學習筆記(一)—— JAVA多線程編程


這個專題主要討論並發編程的問題,所有的討論都是基於JAVA語言的(因其獨特的內存模型以及原生對多線程的支持能力),不過本文傳達的是一種分析的思路,任何有經驗的朋友都能很輕松地將其擴展到任何一門語言。 

注:本文的主要參考資料為結城浩所著《JAVA多線程設計模式》。 

線程的英文名Thread,原意指“細絲”。在多線程程序中,若要追蹤各個線程的軌跡,就會派生出一系列錯綜復雜的亂線團。假設在運行過程中,如果有人問到“請問現在執行到代碼的哪一部分了?”,你需要多個手指頭才能指出正確的地方。 

當應用程序的規模、復雜程度達到一定程度時,並發設計是一個必將考慮到的問題,以下是一些常見的應用: 

  • GUI:以word為例,我們正在編輯一份大型的文檔,此時執行“查找”操作;當word進行查找時,同時會出現一個“停止查找”的按鈕,用戶可以隨時停止。此時就用到了多線程,其中一個線程在后台執行查找,另一個線程顯示“停止查找”的按鈕,一旦按下,則立即停止查找。兩個操作交由不同的線程來處理,各線程可以專心負責自己的功能,因此也是模塊化設計思想的一種體現。
  • 比較耗時的I/O處理:由於磁盤、網絡的IO操作消耗的時間遠大於內存處理,如果在此段時間內,程序僅僅是等待而無法執行其它處理,性能會大打折扣。如果事先能將I/O處理和非I/O處理區分開來,這樣就能夠利用進行I/O處理時CPU空閑的間隙,進行其它處理了。
  • 一個Server與多個Client:大部分Server都要求能夠同時服務於1個以上的Client。Server本身並不知道何時會有Client接入,並且在Server中直接引入多個Client的設計,並不是十分優雅的方案;因此不妨設計成一旦有Client連接到Server,就會生成自動出來迎接這個Client的線程。這樣一來,Server端的程序就可以簡單地設計成好像只服務一個Client。當然,從J2SE 1.4開始,已經加入了新的NIO類庫,不必利用線程也能進行兼具性能和擴充性的I/O處理,詳情可參考JDK。


至於JAVA中線程的編碼方式,無非是繼承自抽象類Thread或者實現Runnable接口,想必各位讀者都很熟悉了,這里就不復述了。 

在多線程程序里,多個線程既然可以自由操作,當然就可能同時操作到同一實例。這個情況又是會造成不必要的麻煩。例如經典的銀行取款問題,其“確認可用余款”這一部分的代碼應該該為: 

Java代碼   收藏代碼
  1. if(可用余額大於欲提取金額)  
  2. {  
  3.   從可用余額中減掉欲提取金額  
  4. }  


這段邏輯本身並沒有問題。先確認可用余額,再檢查是否允許提取輸入金額,如果系統決定可以領取,則從可用余額中減掉此金額,保證不會發生可用余額變為負數的情況。 

但是,同時若有2個以上的線程執行這個程序的代碼,可用余額可能會變成負數。比如: 

Java代碼   收藏代碼
  1. 初始化……  
  2. 可用余額 = 1000元  
  3. 欲提取金額 = 1000元  
  4. 線程A——可用余額大於欲提取金額?  
  5. 線程A——是  
  6. <<<此時切換成線程B>>>  
  7. 線程B——可用余額大於欲提取金額?  
  8. 線程B——是  
  9. 線程B——從可用余額中減掉欲提取金額  
  10. 線程B——可用余額變為0元  
  11. <<<此時切換成線程A>>>  
  12. 線程A——從可用余額中減掉欲提取金額  
  13. 線程A——可用余額變為-1000元  


我們發現,由於時間差,可能會發生線程B夾在線程A的“確認可用余額”和“減去可用余額”之間的情況,這就會導致出現金額為負數的情況。 

JAVA中使用synchronized來實現共享互斥,這就好比十字路口的紅綠燈處理一樣;當直向行車時綠燈時,另一邊的橫向車燈一定是紅燈。synchronized也采用類似的“交通管制”的方式來實現線程間的互斥。 

上述銀行存取款的互斥實現如下 

Java代碼   收藏代碼
  1. public class Bank  
  2. {  
  3.     private int money;  
  4.     private String name;  
  5.   
  6.     public Bank(String name, int money)  
  7.     {  
  8.         this.money = money;  
  9.         this.name = name;  
  10.     }  
  11.   
  12.     // 存款  
  13.     public synchronized void deposit(int m)  
  14.     {  
  15.         money += m;  
  16.     }  
  17.   
  18.     // 取款  
  19.     public synchronized void withdraw(int m)  
  20.     {  
  21.         if (money >= m)  
  22.         {  
  23.             money -= m;  
  24.             return true;  // 已取款  
  25.         }  
  26.         else  
  27.         {  
  28.             return false// 余額不足  
  29.         }  
  30.     }  
  31.   
  32.     public String getName()  
  33.     {  
  34.         return name;  
  35.     }  
  36. }  


當一個線程正在執行Bank實例的deposit或withdraw方法時,其他線程就不能執行同一實例的deposit以及withdraw方法。欲執行的線程必須排隊等候。 

也許會注意到,Bank類里還有一個非synchronized的方法——getName。無論其它線程是否正在執行同一實例的deposit、withdraw或者getName方法,都不妨礙它的執行。 

synchronized阻擋的幾種使用方式,如下 
synchronized局部阻擋:如果需要“管制”的不是整個方法,而是方法的一部分,就使用此類阻擋,代碼如下 

Java代碼   收藏代碼
  1. synchronized(表達式)  
  2. {  
  3.     ……  
  4. }  



synchronized實例方法阻擋:如果需要“管制”的是整個實例方法,而是方法的一部分,就使用此類阻擋,代碼如下 

Java代碼   收藏代碼
  1. synchronized void method()  
  2. {  
  3.     ……  
  4. }  


這段代碼在功能上與如下代碼有異曲同工之妙 

Java代碼   收藏代碼
  1. void method()  
  2. {  
  3.     synchronized(this)      
  4.     {  
  5.         ……  
  6.     }  
  7. }  



synchronized類方法阻擋:如果需要“管制”的是類方法,就使用此類阻擋,代碼如下 

Java代碼   收藏代碼
  1. class Something  
  2. {  
  3.     static synchronized void method()  
  4.     {  
  5.         ……  
  6.     }  
  7. }  


這段代碼在功能上與如下代碼有異曲同工之妙 

Java代碼   收藏代碼
  1. class Something  
  2. {  
  3.     static void method()  
  4.     {  
  5.         synchronized(Something.class)  
  6.         {  
  7.             ……  
  8.         }  
  9.     }  
  10. }  


從上面可以看出,synchronized類阻擋其實質是用類對象作為鎖定的對象去進行互斥的。 

線面講講線程的協調。上面所說的是最簡單的共享互斥,只要有線程再執行就乖乖地等候;現實工作中,我們需要的往往不止於此,比如: 

  • 若空間有空閑則繼續寫入,若沒有則等候。
  • 空間有空閑時,及時通知等待線程。


線程協調的具體實現將在下一章中介紹。 

JAVA中提供了wait、notify、notifyAll以支持此類條件處理。這里要注意到以下幾點: 

  • 如欲執行某一實例的wait方法,則首先必須獲得該實例的鎖;一旦進入wait set(線程的休息間),則自動釋放該鎖。
  • 使用notify方法時,會從鎖定實例的wait set中喚醒一個線程。同樣的,線程必須首先獲得鎖定,才能調用notyfy方法。被喚醒的線程並不是立即就可以執行的,因為在此刻,notify的線程還一直占有鎖。另外,假設執行notify時,wait set里有多於一個的線程在等待,具體選擇那個線程是無法得知的,因此應用程序最好不要寫成會因所選線程而有所變動的方式。
  • notifyAll與notify基本相同,唯一區別在於它是喚醒所有線程而非一個。一般來說,使用notifyAll寫的程序會更健壯一點。

轉載:http://grunt1223.iteye.com/blog/876245


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM