0 前言
上一節(jié)講了Synchronized關(guān)鍵詞的原理與優(yōu)化分析,而配合Synchronized使用的另外兩個關(guān)鍵詞wait¬ify是本章講解的重點。最簡單的東西,往往包含了最復雜的實現(xiàn),因為需要為上層的存在提供一個穩(wěn)定的基礎(chǔ),Object作為Java中所有對象的基類,其存在的價值不言而喻,其中wait¬ify方法的實現(xiàn)多線程協(xié)作提供了保證。
1 源碼
今天我們要學習或者說分析的是 Object 類中的 wait¬ify 這兩個方法,其實說是兩個方法,這兩個方法包括他們的重載方法一共有 5 個,而 Object 類中一共才 12 個方法,可見這 2 個方法的重要性。我們先看看 JDK 中的代碼:
public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
// 此處對于納秒的處理不精準,只是簡單增加了1毫秒,
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
就是這五個方法。其中有 3 個方法是 native 的,也就是由虛擬機本地的 c 代碼執(zhí)行的。有 2 個 wait 重載方法最終還是調(diào)用了 wait(long) 方法。
wait方法:wait是要釋放對象鎖,進入等待池。既然是釋放對象鎖,那么肯定是先要獲得鎖。所以wait必須要寫在synchronized代碼塊中,否則會報異常。
-
notify方法:也需要寫在synchronized代碼塊中,調(diào)用對象的這兩個方法也需要先獲得該對象的鎖。notify,notifyAll,喚醒等待該對象同步鎖的線程,并放入該對象的鎖池中。對象的鎖池中線程可以去競爭得到對象鎖,然后開始執(zhí)行。
- 如果是通過notify來喚起的線程,那先進入wait的線程會先被喚起來,并非隨機喚醒;
- 如果是通過nootifyAll喚起的線程,默認情況是最后進入的會先被喚起來,即LIFO的策略;
另外一點比較重要,notify,notifyAll調(diào)用時并不會釋放對象鎖。比如以下代碼:
public void test() { Object object = new Object(); synchronized (object){ object.notifyAll(); while (true){ } } }
雖然調(diào)用了notifyAll,但是緊接著進入了一個死循環(huán)。導致一直不能出臨界區(qū),一直不能釋放對象鎖。所以,即使它把所有在等待池中的線程都喚醒放到了對象的鎖池中,但是鎖池中的所有線程都不會運行,因為他們始終拿不到鎖。
2 用法
簡單示例:
public class WaitNotifyCase {
public static void main(String[] args) {
final Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread A is waiting to get lock");
synchronized (lock) {
try {
System.out.println("thread A get lock");
TimeUnit.SECONDS.sleep(1);
System.out.println("thread A do wait method");
lock.wait();
System.out.println("wait end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread B is waiting to get lock");
synchronized (lock) {
System.out.println("thread B get lock");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
System.out.println("thread B do notify method");
}
}
}).start();
}
}
執(zhí)行結(jié)果:
thread A is waiting to get lock
thread A get lock
thread B is waiting to get lock
thread A do wait method
thread B get lock
thread B do notify method
wait end
前提:必須由同一個lock對象調(diào)用wait、notify方法
- 當線程A執(zhí)行wait方法時,該線程會被掛起;
- 當線程B執(zhí)行notify方法時,會喚醒一個被掛起的線程A;
lock對象、線程A和線程B三者是一種什么關(guān)系?根據(jù)上面的結(jié)論,可以想象一個場景:
- lock對象維護了一個等待隊列l(wèi)ist;
- 線程A中執(zhí)行l(wèi)ock的wait方法,把線程A保存到list中;
- 線程B中執(zhí)行l(wèi)ock的notify方法,從等待隊列中取出線程A繼續(xù)執(zhí)行;
3 相關(guān)疑問
3.1 為何wait¬ify必須要加synchronized鎖
從實現(xiàn)上來說,這個鎖至關(guān)重要,正因為這把鎖,才能讓整個wait/notify玩轉(zhuǎn)起來,當然我覺得其實通過其他的方式也可以實現(xiàn)類似的機制,不過hotspot至少是完全依賴這把鎖來實現(xiàn)wait/notify的。
static void Sort(int [] array) {
// synchronize this operation so that some other thread can't
// manipulate the array while we are sorting it. This assumes that other
// threads also synchronize their accesses to the array.
synchronized(array) {
// now sort elements in array
}
}
synchronized
代碼塊通過javap生成的字節(jié)碼中包含 monitorenter
和 monitorexit
指令。如下圖所示:
執(zhí)行 monitorenter
指令可以獲取對象的monitor,而 lock.wait()
方法通過調(diào)用native方法wait(0)實現(xiàn),其中接口注釋中有這么一句:
The current thread must own this object's monitor.
表示線程執(zhí)行 lock.wait()
方法時,必須持有該lock對象的monitor,如果wait方法在synchronized代碼中執(zhí)行,該線程很顯然已經(jīng)持有了monitor。
3.2 為什么wait方法可能拋出InterruptedException異常
這個異常大家應該都知道,當我們調(diào)用了某個線程的interrupt方法時,對應的線程會拋出這個異常,wait方法也不希望破壞這種規(guī)則,因此就算當前線程因為wait一直在阻塞,當某個線程希望它起來繼續(xù)執(zhí)行的時候,它還是得從阻塞態(tài)恢復過來,因此wait方法被喚醒起來的時候會去檢測這個狀態(tài),當有線程interrupt了它的時候,它就會拋出這個異常從阻塞狀態(tài)恢復過來。
這里有兩點要注意:
如果被interrupt的線程只是創(chuàng)建了,并沒有start,那等他start之后進入wait態(tài)之后也是不能會恢復的;
如果被interrupt的線程已經(jīng)start了,在進入wait之前,如果有線程調(diào)用了其interrupt方法,那這個wait等于什么都沒做,會直接跳出來,不會阻塞;
3.3 notify執(zhí)行之后立馬喚醒線程嗎
其實hotspot里真正的實現(xiàn)是退出同步塊的時候才會去真正喚醒對應的線程,不過這個也是個默認策略,也可以改的,在notify之后立馬喚醒相關(guān)線程。
3.4 notifyAll是怎么實現(xiàn)全喚起所有線程
或許大家立馬想到這個簡單,一個for循環(huán)就搞定了,不過在JVM里沒實現(xiàn)這么簡單,而是借助了monitorexit,上面提到了當某個線程從wait狀態(tài)恢復出來的時候,要先獲取鎖,然后再退出同步塊,所以notifyAll的實現(xiàn)是調(diào)用notify的線程在退出其同步塊的時候喚醒起最后一個進入wait狀態(tài)的線程,然后這個線程退出同步塊的時候繼續(xù)喚醒其倒數(shù)第二個進入wait狀態(tài)的線程,依次類推,同樣這這是一個策略的問題,JVM里提供了挨個直接喚醒線程的參數(shù),不過都很罕見就不提了。
3.5 wait的線程是否會影響load
這個或許是大家比較關(guān)心的話題,因為關(guān)乎系統(tǒng)性能問題,wait/nofity
是通過JVM里的 park/unpark
機制來實現(xiàn)的,在Linux下這種機制又是通過
pthread_cond_wait/pthread_cond_signal
來玩的,因此當線程進入到wait狀態(tài)的時候其實是會放棄cpu的,也就是說這類線程是不會占用cpu資源。