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

前言

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

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

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

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

   

忙等待

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

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


wait(),notify()和notifyAll()

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

//是否可借
  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時(書本已經(jīng)被小A借走),小A 還書(調(diào)用giveBackBook方法)之后,書的可借狀態(tài)為可借,并且調(diào)用notify()通知等待線程(系統(tǒng)發(fā)短信給小B)需要注意的兩點(diǎn)是:

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

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

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

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

丟失的信號

學(xué)校圖書館系統(tǒng)是這么設(shè)計(jì)的,當(dāng)一本書被還回來的時候,會給等待著發(fā)送短信,并且只會發(fā)一次,如果沒有等待者,他也會發(fā)(只不過沒有接受者)。問題出現(xiàn)了,因?yàn)槎绦胖粫l(fā)一次,當(dāng)書被還回來的時候,沒有人等待借書,他會發(fā)一條空短信,但是之后有等待借此本書的同學(xué)永遠(yuǎn)也不會再收到短信,導(dǎo)致這些同學(xué)會無休止的等待,為了避免這個問題,我們等待的時候先打個電話問問圖書管理員是否繼續(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()方法不會保存調(diào)用它們的方法,因?yàn)楫?dāng)這兩個方法被調(diào)用時,有可能沒有線程處于等待狀態(tài),通知信號過后便丟棄了。因此,如果一個線程先于被通知線程調(diào)用wait()前調(diào)用notify(),等待的線程就將錯過這個信號。在某些情況下,這可能使得等待線程永久等待,不再被喚醒。所以,為了避免丟失信號,我們需要將信號保存到信號類里。這里的管理員就是信號類。
假喚醒

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

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

   

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

因?yàn)槿绻褂贸A孔址脑?。JVM/編譯器內(nèi)部會把常量字符串轉(zhuǎn)成同一個對象,
這意味著即使你有2個不同的MyWaitNotify3實(shí)例,它們都是引用了相同的空字符串實(shí)例。同時也意味著存在這樣的風(fēng)險(xiǎn),在第一個MyWaitNotify3實(shí)例調(diào)用doWait()會被第二個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é)

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


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