前言
上一節講了Synchronized關鍵詞的原理與優化分析,而配合Synchronized使用的另外兩個關鍵詞wait¬ify是本章講解的重點。最簡單的東西,往往包含了最復雜的實現,因為需要為上層的存在提供一個穩定的基礎,Object作為Java中所有對象的基類,其存在的價值不言而喻,其中wait¬ify方法的實現多線程協作提供了保證。
1 源碼
今天我們要學習或者說分析的是 Object 類中的 wait¬ify 這兩個方法,其實說是兩個方法,這兩個方法包括他們的重載方法一共有 5 個,而 Object 類中一共才 12 個方法,可見這 2 個方法的重要性。我們先看看 JDK 中的代碼:

就是這五個方法。其中有 3 個方法是 native 的,也就是由虛擬機本地的 c 代碼執行的。有 2 個 wait 重載方法最終還是調用了 wait(long) 方法。
1.wait方法:wait是要釋放對象鎖,進入等待池。既然是釋放對象鎖,那么肯定是先要獲得鎖。所以wait必須要寫在synchronized代碼塊中,否則會報異常。
2.notify方法:也需要寫在synchronized代碼塊中,調用對象的這兩個方法也需要先獲得該對象的鎖。notify,notifyAll,喚醒等待該對象同步鎖的線程,並放入該對象的鎖池中。對象的鎖池中線程可以去競爭得到對象鎖,然后開始執行。

另外一點比較重要,notify,notifyAll調用時並不會釋放對象鎖。比如以下代碼:

雖然調用了notifyAll,但是緊接着進入了一個死循環。導致一直不能出臨界區,一直不能釋放對象鎖。所以,即使它把所有在等待池中的線程都喚醒放到了對象的鎖池中,但是鎖池中的所有線程都不會運行,因為他們始終拿不到鎖。
2 用法
簡單示例:

執行結果:

前提:必須由同一個lock對象調用wait、notify方法

lock對象、線程A和線程B三者是一種什么關系?根據上面的結論,可以想象一個場景:

3 相關疑問
3.1 為何wait¬ify必須要加synchronized鎖
從實現上來說,這個鎖至關重要,正因為這把鎖,才能讓整個wait/notify玩轉起來,當然我覺得其實通過其他的方式也可以實現類似的機制,不過hotspot至少是完全依賴這把鎖來實現wait/notify的。

synchronized 代碼塊通過javap生成的字節碼中包含 monitorenter 和 monitorexit 指令。如下圖所示:

執行 monitorenter 指令可以獲取對象的monitor,而 lock.wait() 方法通過調用native方法wait(0)實現,其中接口注釋中有這么一句:

表示線程執行 lock.wait() 方法時,必須持有該lock對象的monitor,如果wait方法在synchronized代碼中執行,該線程很顯然已經持有了monitor。
3.2 為什么wait方法可能拋出InterruptedException異常
這個異常大家應該都知道,當我們調用了某個線程的interrupt方法時,對應的線程會拋出這個異常,wait方法也不希望破壞這種規則,因此就算當前線程因為wait一直在阻塞,當某個線程希望它起來繼續執行的時候,它還是得從阻塞態恢復過來,因此wait方法被喚醒起來的時候會去檢測這個狀態,當有線程interrupt了它的時候,它就會拋出這個異常從阻塞狀態恢復過來。
這里有兩點要注意:

3.3 notify執行之后立馬喚醒線程嗎
其實hotspot里真正的實現是退出同步塊的時候才會去真正喚醒對應的線程,不過這個也是個默認策略,也可以改的,在notify之后立馬喚醒相關線程。
3.4 notifyAll是怎么實現全喚起所有線程
或許大家立馬想到這個簡單,一個for循環就搞定了,不過在JVM里沒實現這么簡單,而是借助了monitorexit,上面提到了當某個線程從wait狀態恢復出來的時候,要先獲取鎖,然后再退出同步塊,所以notifyAll的實現是調用notify的線程在退出其同步塊的時候喚醒起最后一個進入wait狀態的線程,然后這個線程退出同步塊的時候繼續喚醒其倒數第二個進入wait狀態的線程,依次類推,同樣這這是一個策略的問題,JVM里提供了挨個直接喚醒線程的參數,不過都很罕見就不提了。
3.5 wait的線程是否會影響load
這個或許是大家比較關心的話題,因為關乎系統性能問題,wait/nofity是通過JVM里的park/unpark機制來實現的,在Linux下這種機制又是通過
pthread_cond_wait/pthread_cond_signal來玩的,因此當線程進入到wait狀態的時候其實是會放棄cpu的,也就是說這類線程是不會占用cpu資源