1. 程式人生 > >wait notify notifyAll await signal signalAll 的理解及示例

wait notify notifyAll await signal signalAll 的理解及示例

從常見的一道面試題開始,題目的描述是這樣子的:

有三個執行緒分別列印A、B、C,請用多執行緒程式設計實現,在螢幕上迴圈列印10次ABCABC…

網上大都教了你怎麼去實現,其實我也寫過一篇 https://blog.csdn.net/sanri1993/article/details/89644493 但是都沒有把原理說透,說再多的解法別人也記不住。

這個其實需要從最原本的 Object 的方法 wait() ,notify() notifyAll() 來理解 ,想必讀者在工作中應該幾乎是沒有使用過這幾個方法的,這裡我稍微介紹下功能

  • 這幾個方法都必須線上程獲得鎖之後才能呼叫(這裡說的鎖是 synchronized
    鎖)
  • wait 方法會阻塞當前執行緒並釋放鎖,直到被喚醒。即另一個執行緒獲得那把鎖,並呼叫鎖物件的 notify 或 notifyAll 方法
  • notify 呼叫後,wait 方法並不是立馬往後執行,它需要重新獲取鎖
  • wait 呼叫後會把當前執行緒放進一個 wait 集合中,那個集合並不是有序的

利用 wait 的阻塞特性,我們可以用它來實現迴圈列印 ABC ;可以使用三把鎖來實現,還需要一個變數來控制當前應該列印誰,當前如果不是列印這個值時,呼叫 wait,如果是列印這個值,就列印這個值並切換成一個列印變數,同時喚醒下一個列印,虛擬碼如下:

完整程式碼 ABCThreadWait 在 這個地方

char currentChar = 'A';
Object lockA = new Object();
Object lockB = new Object();
Object lockC = new Object();

ThreadA
    synchronized(lockA){
        // 先獲取 A 鎖,可能馬上就要呼叫 wait 方法
        // 因為這裡只有三個執行緒,所以用 if 和 while 是一樣的,建議用 while 
        if(currentChar != 'A'){
            lockA.wait();
        }
        print('A');currentChar = 'B';
        //然後喚醒 B ,需要先獲取B 鎖
        synchronized(lockB){
            lockB.notify();
        }
    }
ThreadB
    synchronized(lockB){
        if(currentChar != 'B'){
            lockB.wait();
        }
        print('B');currentChar = 'C';
        synchronized(lockC){
            lockC.notify();
        }
    }
ThreadC
    synchronized(lockC){
        if(currentChar != 'C'){
            lockC.wait();
        }
        print('C');currentChar = 'A';
        synchronized(lockA){
            lockA.notify();
        }
    }

這裡可以精簡為使用一個執行緒類,使用不同的執行緒例項來實現,但還是使用的三把鎖,每個執行緒都需要先獲取自身的鎖,然後判斷是否需要列印,如果不是當前字元則釋放鎖;是當前字元就列印字元,在列印完後獲取下一個列印的鎖,把下一個列印執行緒喚醒。虛擬碼如下:

完整程式碼 ABCThreadWaitOneThreadClass 在 這個地方

synchronized (lockSelf){
    if(printChar != currentPrintChar){
        try {
            lockSelf.wait();
        } catch (InterruptedException e) {e.printStackTrace();}
    }
    // 列印當前執行緒字元
    System.out.print(printChar);

    // 切換下一個執行緒,並切換狀態
    if(currentPrintChar == 'A'){currentPrintChar = 'B';}
    else if(currentPrintChar == 'B'){currentPrintChar = 'C';}
    else if(currentPrintChar == 'C'){currentPrintChar = 'A';}

    //喚醒下一個執行緒
    synchronized (lockNext) {
        lockNext.notify();
    }
}

當然也可以使用一把鎖來實現,這需要用到 Lock + Condition ,關於 Lock 和 Condition 見這篇文章

使用 Condition 的 await signal signalAll 時,同樣需要獲得 Lock 鎖,其它特性等同於 wait notify notifyAll

其實仔細看這道題,永遠只有一個執行緒在列印,照理論來說只需要一把鎖即可,上面需要有多把鎖的原因是一個鎖只有一個等待佇列 ,並且 notify 也是隨機喚醒的。而每個 Condition 會帶一個等待佇列,所以用 Condition 只要一把鎖就可以了,減輕了程式碼的複雜度,多鎖情況很容易造成死鎖。

使用 Lock + Condition 的完整程式碼 ConditionABC 在 這個地方 ,這是用多個執行緒類實現的,當然也可以用單個執行緒類多個執行緒例項來實現,這裡就不再寫了。

借用一篇寫得挺不錯的博文 ,請一定耐心把它讀完再接著往下讀 你真的懂 wait notify notifyAll 嗎

文章中的原始碼QueueUseWaitNotify 在 這個地方

這個圖不錯,收藏了

文章中有一個地方說得挺好,就是面試常問的 notify 和 notifyAll 的區別

青銅玩家會一臉純真的看著面試官,就是喚醒一個和喚醒一堆啊,但它兩真正的區別是 notifyAll 呼叫後,會把所有在 Wait Set 中的執行緒狀態變成 RUNNABLE 狀態,然後這些執行緒再去競爭鎖,獲取到鎖的執行緒為 Run 狀態,沒有獲取到鎖的執行緒進入 Entry Set 集合中變成 Block 狀態,它們只需要等到上個執行緒執行完或者 wait 就可以再次競爭鎖而無需 notify ; 而 notify 方法只是照規則喚醒 Wait Set 中的某一個執行緒,其它的執行緒還是在 Wait Set 中。

文章中說到的為什麼 wait 要寫在 for 迴圈中是因為 wait 是釋放了鎖,然後阻塞,等到下次喚醒的時候,在多個生產者多個消費者的情況下,有可能是被 “同類” 喚醒的,所以需要再去檢查下狀態是否正確。

文章中有一個地方沒有說明白 ,這裡再解釋下,就是那個使用 notfiy 會帶來死鎖的問題,個人理解,如有偏差望指正

當有多個消費者和多個生產者的時候,這時正好在消費,所以生產者是在 Wait Set 中,可能還有其它消費者也在 Wait Set 中,因為是 notify 而不是 notfiyAll 嘛,所以消費者有可能一直 notify 的都是另一個消費者,剛好這時 buffer 空了,正好所有消費都 wait 了而沒能及時 notify 生產者,這時 Wait Set 中四目相望造成死鎖。

文章最後有一個評論說可以生產者用一把鎖,消費者用一把鎖,這裡也有實現 QueueUseWaitNofiy2

可以使用 Condition 做更好的實現,只使用一把鎖,這裡本身也只需要一把鎖就可以了,具體實現見程式碼 QueueUseCondition

一點小推廣

創作不易,希望可以支援下我的開源軟體,及我的小工具,歡迎來 gitee 點星,fork ,提 bug 。

Excel 通用匯入匯出,支援 Excel 公式
部落格地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi

使用模板程式碼 ,從資料庫生成程式碼 ,及一些專案中經常可以用到的小工具
部落格地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-ma