0 前言
上一節講了Synchronized關鍵詞的原理與優化分析,而配合Synchronized使用的另外兩個關鍵詞wait¬ify是本章講解的重點。最簡單的東西,往往包含了最複雜的實現,因為需要為上層的存在提供一個穩定的基礎,Object作為Java中所有物件的基類,其存在的價值不言而喻,其中wait¬ify方法的實現多執行緒協作提供了保證。
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 程式碼執行的。有 2 個 wait 過載方法最終還是呼叫了 wait(long) 方法。
-
wait方法:wait是要釋放物件鎖,進入等待池。既然是釋放物件鎖,那麼肯定是先要獲得鎖。所以wait必須要寫在synchronized程式碼塊中,否則會報異常。
-
notify方法:也需要寫在synchronized程式碼塊中,呼叫物件的這兩個方法也需要先獲得該物件的鎖。notify,notifyAll,喚醒等待該物件同步鎖的執行緒。notify喚醒物件等待池中的一個執行緒,將這個執行緒放入該物件的鎖池中。物件的鎖池中執行緒可以去競爭得到物件鎖,然後開始執行。
- 如果是通過notify來喚起的執行緒,那先進入wait的執行緒會先被喚起來,並非隨機喚醒;
- 如果是通過nootifyAll喚起的執行緒,預設情況是最後進入的會先被喚起來,即LIFO的策略;
另一點,notify,notifyAll呼叫時並不會釋放物件鎖。比如以下程式碼:
public void test() { Object object = new Object(); synchronized (object){ object.notifyAll(); while (true){ } } } 複製程式碼
雖然呼叫了notifyAll,但是緊接著進入了一個死迴圈。導致一直不能出臨界區,一直不能釋放物件鎖。所以,即使它把所有在等待池中的執行緒都喚醒放到了物件的鎖池中,但是鎖池中的所有執行緒都不會執行,因為他們都拿不到鎖。
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();
}
}
複製程式碼
執行結果:
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物件呼叫wait、notify方法
- 當執行緒A執行wait方法時,該執行緒會被掛起;
- 當執行緒B執行notify方法時,會喚醒一個被掛起的執行緒A;
lock物件、執行緒A和執行緒B三者是一種什麼關係?根據上面的結論,可以想象一個場景:
- lock物件維護了一個等待佇列list;
- 執行緒A中執行lock的wait方法,把執行緒A儲存到list中;
- 執行緒B中執行lock的notify方法,從等待佇列中取出執行緒A繼續執行;