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-maven


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM