這個專題主要討論並發編程的問題,所有的討論都是基於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接口,想必各位讀者都很熟悉了,這里就不復述了。
在多線程程序里,多個線程既然可以自由操作,當然就可能同時操作到同一實例。這個情況又是會造成不必要的麻煩。例如經典的銀行取款問題,其“確認可用余款”這一部分的代碼應該該為:
- if(可用余額大於欲提取金額)
- {
- 從可用余額中減掉欲提取金額
- }
這段邏輯本身並沒有問題。先確認可用余額,再檢查是否允許提取輸入金額,如果系統決定可以領取,則從可用余額中減掉此金額,保證不會發生可用余額變為負數的情況。
但是,同時若有2個以上的線程執行這個程序的代碼,可用余額可能會變成負數。比如:
- 初始化……
- 可用余額 = 1000元
- 欲提取金額 = 1000元
- 線程A——可用余額大於欲提取金額?
- 線程A——是
- <<<此時切換成線程B>>>
- 線程B——可用余額大於欲提取金額?
- 線程B——是
- 線程B——從可用余額中減掉欲提取金額
- 線程B——可用余額變為0元
- <<<此時切換成線程A>>>
- 線程A——從可用余額中減掉欲提取金額
- 線程A——可用余額變為-1000元
我們發現,由於時間差,可能會發生線程B夾在線程A的“確認可用余額”和“減去可用余額”之間的情況,這就會導致出現金額為負數的情況。
JAVA中使用synchronized來實現共享互斥,這就好比十字路口的紅綠燈處理一樣;當直向行車時綠燈時,另一邊的橫向車燈一定是紅燈。synchronized也采用類似的“交通管制”的方式來實現線程間的互斥。
上述銀行存取款的互斥實現如下
- public class Bank
- {
- private int money;
- private String name;
- public Bank(String name, int money)
- {
- this.money = money;
- this.name = name;
- }
- // 存款
- public synchronized void deposit(int m)
- {
- money += m;
- }
- // 取款
- public synchronized void withdraw(int m)
- {
- if (money >= m)
- {
- money -= m;
- return true; // 已取款
- }
- else
- {
- return false; // 余額不足
- }
- }
- public String getName()
- {
- return name;
- }
- }
當一個線程正在執行Bank實例的deposit或withdraw方法時,其他線程就不能執行同一實例的deposit以及withdraw方法。欲執行的線程必須排隊等候。
也許會注意到,Bank類里還有一個非synchronized的方法——getName。無論其它線程是否正在執行同一實例的deposit、withdraw或者getName方法,都不妨礙它的執行。
synchronized阻擋的幾種使用方式,如下
synchronized局部阻擋:如果需要“管制”的不是整個方法,而是方法的一部分,就使用此類阻擋,代碼如下
- synchronized(表達式)
- {
- ……
- }
synchronized實例方法阻擋:如果需要“管制”的是整個實例方法,而是方法的一部分,就使用此類阻擋,代碼如下
- synchronized void method()
- {
- ……
- }
這段代碼在功能上與如下代碼有異曲同工之妙
- void method()
- {
- synchronized(this)
- {
- ……
- }
- }
synchronized類方法阻擋:如果需要“管制”的是類方法,就使用此類阻擋,代碼如下
- class Something
- {
- static synchronized void method()
- {
- ……
- }
- }
這段代碼在功能上與如下代碼有異曲同工之妙
- class Something
- {
- static void method()
- {
- synchronized(Something.class)
- {
- ……
- }
- }
- }
從上面可以看出,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