天天看點

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

這個專題主要讨論并發程式設計的問題,所有的讨論都是基于JAVA語言的(因其獨特的記憶體模型以及原生對多線程的支援能力),不過本文傳達的是一種分析的思路,任何有經驗的朋友都能很輕松地将其擴充到任何一門語言。

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

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

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

[list]

[*][b]GUI[/b]:以word為例,我們正在編輯一份大型的文檔,此時執行“查找”操作;當word進行查找時,同時會出現一個“停止查找”的按鈕,使用者可以随時停止。此時就用到了多線程,其中一個線程在背景執行查找,另一個線程顯示“停止查找”的按鈕,一旦按下,則立即停止查找。兩個操作交由不同的線程來處理,各線程可以專心負責自己的功能,是以也是子產品化設計思想的一種展現。

[*][b]比較耗時的I/O處理[/b]:由于磁盤、網絡的IO操作消耗的時間遠大于記憶體處理,如果在此段時間内,程式僅僅是等待而無法執行其它處理,性能會大打折扣。如果事先能将I/O處理和非I/O處理區分開來,這樣就能夠利用進行I/O處理時CPU空閑的間隙,進行其它處理了。

[*][b]一個Server與多個Client[/b]:大部分Server都要求能夠同時服務于1個以上的Client。Server本身并不知道何時會有Client接入,并且在Server中直接引入多個Client的設計,并不是十分優雅的方案;是以不妨設計成一旦有Client連接配接到Server,就會生成自動出來迎接這個Client的線程。這樣一來,Server端的程式就可以簡單地設計成好像隻服務一個Client。當然,從J2SE 1.4開始,已經加入了新的NIO類庫,不必利用線程也能進行兼具性能和擴充性的I/O處理,詳情可參考JDK。

[/list]

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

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

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

但是,同時若有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類阻擋其實質是用類對象作為鎖定的對象去進行互斥的。

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

[list]

[*]若空間有空閑則繼續寫入,若沒有則等候。

[*]空間有空閑時,及時通知等待線程。

[/list]

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

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

[list]

[*]如欲執行某一執行個體的wait方法,則首先必須獲得該執行個體的鎖;一旦進入wait set(線程的休息間),則自動釋放該鎖。

[*]使用notify方法時,會從鎖定執行個體的wait set中喚醒一個線程。同樣的,線程必須首先獲得鎖定,才能調用notyfy方法。被喚醒的線程并不是立即就可以執行的,因為在此刻,notify的線程還一直占有鎖。另外,假設執行notify時,wait set裡有多于一個的線程在等待,具體選擇那個線程是無法得知的,是以應用程式最好不要寫成會因所選線程而有所變動的方式。

[*]notifyAll與notify基本相同,唯一差別在于它是喚醒所有線程而非一個。一般來說,使用notifyAll寫的程式會更健壯一點。

[/list]