Java并發(fā)編程(五)---線程通信

前言

上一篇我們介紹了死鎖的發(fā)生條件,以及避免死鎖的方式。其中 破壞占有且等待的處理是,通過(guò)一個(gè)單例類(lèi)一次性申請(qǐng)所有資源,直到成功。如while (!Allocator.getAllocator().applyResource(this, target)) { return; } 如果在并發(fā)量比較小的情況下,還可以接受,如果并發(fā)量比較大的話,就會(huì)大量的消耗CPU的資源。這時(shí)候,我們應(yīng)該引入線程通信,主要是 等待-喚醒機(jī)制。
線程通信

下面我們通過(guò)一個(gè)例子來(lái)說(shuō)明。場(chǎng)景說(shuō)明:
圖書(shū)館里,有一本書(shū)叫《Java 高并發(fā)實(shí)戰(zhàn)》,小A早上的時(shí)候把這本書(shū)借走了,小B中午的時(shí)候去圖書(shū)館找這本書(shū),這里小A和小B分別是兩個(gè)線程,他們都要看的書(shū)是共享資源。
通過(guò)共享對(duì)象通信

小B去了圖書(shū)館,發(fā)現(xiàn)這本書(shū)被借走了,他回到家,等了幾天,再去圖書(shū)館找這本書(shū),發(fā)現(xiàn)這本書(shū)已經(jīng)被歸還了,他順利借走了書(shū)。
用程序模擬的話,就是線程小A調(diào)用setCanBorrow方法將canBorrow設(shè)為true,線程小B調(diào)用getCanBorrow獲取。在這里線程A和B必須獲得
指向同一個(gè)LibraryTest1共享實(shí)例的引用,如果持有的對(duì)象指向不同的LibraryTest1實(shí)例,那么彼此將不能檢測(cè)到對(duì)方的信號(hào)。

public class LibraryTest1 {
    //是否可借
    private boolean canBorrow = false;
    synchronized boolean getCanBorrow() {
        return canBorrow;
    }
    synchronized void setCanBorrow(boolean canBorrow) {
        this.canBorrow = canBorrow;
    }
}

   

忙等待

其實(shí)小A在小B走后一會(huì)就把書(shū)還回去了,小B卻在幾天后才去找書(shū),為了早點(diǎn)借到書(shū)(減少延遲),小B可能在圖書(shū)館等著,每隔幾分鐘(while循環(huán)),他就去檢查這本書(shū)有沒(méi)有被還回,這樣只要小A一還回書(shū),小B馬上就會(huì)知道。

public class LibraryTest1 {
         final LibraryTest1 libraryTest1 = new LibraryTest1();
           while (!libraryTest1.getCanBorrow()) {
            //空等
             return;
        }
}


wait(),notify()和notifyAll()

很多次后,小B發(fā)現(xiàn)自己這樣做太累了,身體有點(diǎn)吃不消,不過(guò)很快,學(xué)校圖書(shū)館系統(tǒng)改進(jìn),加入了短信通知功能(notify()),只要小A一還回書(shū),圖書(shū)館會(huì)立馬通知小B,這樣小B就可以在家睡覺(jué)等短信了。

//是否可借
  private boolean canBorrow = false;

    synchronized String borrowBook() throws InterruptedException {
        if (!canBorrow) {
            wait();
            return null;
        }
        canBorrow = false;
        return "Java 高并發(fā)實(shí)戰(zhàn)";
    }

    synchronized void giveBackBook() {
        this.canBorrow = true;
        notify();
    }


如上程序,canBorrow 初始為false時(shí)(書(shū)本已經(jīng)被小A借走),小A 還書(shū)(調(diào)用giveBackBook方法)之后,書(shū)的可借狀態(tài)為可借,并且調(diào)用notify()通知等待線程(系統(tǒng)發(fā)短信給小B)需要注意的兩點(diǎn)是:

    wait(),notify() 和或者notifyAll() 都需要在同步代碼塊中調(diào)用(就是消息只能由圖書(shū)管理系統(tǒng)發(fā)出),不能再同步代碼塊之外調(diào)用,否則,會(huì)拋出IllegalMonitorStateException異常。
    notify() 是隨機(jī)的通知等待隊(duì)列里的某一個(gè)線程,而notifyAll()是通知等待隊(duì)列里的所有線程。一般而言最好調(diào)用notifyAll(),而不要調(diào)用notify()。因?yàn)椋ㄖ獣r(shí),只是表示通知時(shí)間點(diǎn)條件滿足,等線程執(zhí)行時(shí),條件可能已經(jīng)不滿足了,線程的執(zhí)行時(shí)間與通知時(shí)間不重合,如果調(diào)用notify()的話很快能不能通知到我們期望通知的線程。

wait()與sleep() 的相同點(diǎn)與不同點(diǎn)

相同點(diǎn):都會(huì)讓當(dāng)前線程掛起一段時(shí)間,讓渡CPU的執(zhí)行時(shí)間,等待再次調(diào)度
不同點(diǎn):

    wait(),notify(),notifyAll()一定是在synchronized{}內(nèi)部調(diào)用,等待和通知的對(duì)象必須要對(duì)應(yīng)。而sleep可以再任何地方調(diào)用
    wait 會(huì)釋放鎖對(duì)象的“鎖標(biāo)志”,當(dāng)調(diào)用某一對(duì)象的wait()方法后,會(huì)使當(dāng)前線程暫停執(zhí)行,并將當(dāng)前線程放入對(duì)象等待池中。知道調(diào)用notifyAll()方法,而sleep則不會(huì)釋放,也就是說(shuō)在休眠期間,其他線程仍然不能訪問(wèn)共享數(shù)據(jù)。
    wait可以被喚醒,sleep的只能等其睡眠結(jié)束
    wait()是在Object 類(lèi)里,而sleep是在Thread 里的

丟失的信號(hào)

學(xué)校圖書(shū)館系統(tǒng)是這么設(shè)計(jì)的,當(dāng)一本書(shū)被還回來(lái)的時(shí)候,會(huì)給等待著發(fā)送短信,并且只會(huì)發(fā)一次,如果沒(méi)有等待者,他也會(huì)發(fā)(只不過(guò)沒(méi)有接受者)。問(wèn)題出現(xiàn)了,因?yàn)槎绦胖粫?huì)發(fā)一次,當(dāng)書(shū)被還回來(lái)的時(shí)候,沒(méi)有人等待借書(shū),他會(huì)發(fā)一條空短信,但是之后有等待借此本書(shū)的同學(xué)永遠(yuǎn)也不會(huì)再收到短信,導(dǎo)致這些同學(xué)會(huì)無(wú)休止的等待,為了避免這個(gè)問(wèn)題,我們等待的時(shí)候先打個(gè)電話問(wèn)問(wèn)圖書(shū)管理員是否繼續(xù)等待if(!wasSignalled)。

public class LibraryTest3 {
    private MonitorObject monitorObject = new MonitorObject();
    private boolean canBorrow = false;
    private boolean wasSignalled = false;

    String borrowBook() throws InterruptedException {
        synchronized (monitorObject) {
            if (!canBorrow||!wasSignalled) {
                wait();
                return null;
            }
            canBorrow = false;
            return "Java 高并發(fā)實(shí)戰(zhàn)";
        }
    }

    void giveBackBook() {
        synchronized (monitorObject) {
            this.canBorrow = true;
            this.wasSignalled = true;
            notifyAll();
        }
    }
}



notify()和notifyAll()方法不會(huì)保存調(diào)用它們的方法,因?yàn)楫?dāng)這兩個(gè)方法被調(diào)用時(shí),有可能沒(méi)有線程處于等待狀態(tài),通知信號(hào)過(guò)后便丟棄了。因此,如果一個(gè)線程先于被通知線程調(diào)用wait()前調(diào)用notify(),等待的線程就將錯(cuò)過(guò)這個(gè)信號(hào)。在某些情況下,這可能使得等待線程永久等待,不再被喚醒。所以,為了避免丟失信號(hào),我們需要將信號(hào)保存到信號(hào)類(lèi)里。這里的管理員就是信號(hào)類(lèi)。
假喚醒

圖書(shū)館系統(tǒng)還有一個(gè)BUG:系統(tǒng)會(huì)偶爾給你發(fā)條錯(cuò)誤短信,說(shuō)書(shū)可以借了(其實(shí)不可以借),我們之前已經(jīng)該圖書(shū)館管理員打過(guò)電話了,他說(shuō)讓我們等短信。我們很聽(tīng)話,一等到短信(其實(shí)是bug引起的錯(cuò)誤短信),就去借書(shū)了,到了圖書(shū)館后發(fā)現(xiàn)這書(shū)根本沒(méi)有還回來(lái)!我們很郁悶,但也沒(méi)有辦法呀,學(xué)校不修復(fù)BUG,我們得聰明點(diǎn):每次在收到短信后,再打電話問(wèn)問(wèn)書(shū)到底能不能借while(!canBorrow||!wasSignalled)。

 String borrowBook() throws InterruptedException {
        synchronized (monitorObject) {
            while (!canBorrow||!wasSignalled) {
                wait();
                return null;
            }
            canBorrow = false;
            return "Java 高并發(fā)實(shí)戰(zhàn)";
        }
    }

   

就像案例里說(shuō)的,有時(shí)候線程會(huì)被莫名奇妙的喚醒,為了防止假喚醒,保存信號(hào)的成員變量將在一個(gè)while循環(huán)里接收檢查。而不是在if表達(dá)式里。
不要對(duì)常量字符串或全局對(duì)象調(diào)用wait()

因?yàn)槿绻褂贸A孔址脑挕VM/編譯器內(nèi)部會(huì)把常量字符串轉(zhuǎn)成同一個(gè)對(duì)象,
這意味著即使你有2個(gè)不同的MyWaitNotify3實(shí)例,它們都是引用了相同的空字符串實(shí)例。同時(shí)也意味著存在這樣的風(fēng)險(xiǎn),在第一個(gè)MyWaitNotify3實(shí)例調(diào)用doWait()會(huì)被第二個(gè)MyWaitNotify3實(shí)例上調(diào)用doNotify()的線程喚醒。

public class MyWaitNotify3 {

    String monitorObject = "";
    boolean isSignaled = false;

    public void doWait() {
        synchronized (monitorObject) {
            while (!isSignaled) {
                try {
                    monitorObject.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //清楚標(biāo)記,繼續(xù)執(zhí)行
            isSignaled = false;
        }
    }

    public void doNotify() {
        synchronized (monitorObject) {
            isSignaled = true;
            monitorObject.notifyAll();
        }
    }

}


總結(jié)

本文主要介紹了線程之間的通信,通過(guò)一個(gè)現(xiàn)實(shí)中的場(chǎng)景再現(xiàn)了這5種場(chǎng)景,其中等待-通知機(jī)制是線程通信的核心機(jī)制。工作中用輪詢的方式來(lái)等待某個(gè)狀態(tài),很多情況下都可以用等待-通知機(jī)制。


作者:碼農(nóng)飛哥
微信公眾號(hào):碼農(nóng)飛哥