java wait與notify
前言
我們知道,java的wait/notify的通知機制可以用來實現執行緒間通訊。wait表示執行緒的等待,呼叫該方法會導致執行緒阻塞,直至另一執行緒呼叫notify或notifyAll方法才可另其繼續執行。經典的生產者、消費者模式即是使用wait/notify機制得以完成。在這篇文章中,我們將深入解析這一機制,瞭解其背後的原理。
執行緒的狀態
在瞭解wait/notify機制前,先熟悉一下java執行緒的幾個生命週期。分別為初始(NEW)、執行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超時等待(TIMED_WAITING)、終止(TERMINATED)等狀態(位於java.lang.Thread.State列舉類中)。
以下是對這幾個狀態的簡要說明,詳細說明見該類註釋。
狀態名稱 | 說明 |
NEW | 初始狀態,執行緒被構建,但未呼叫start()方法 |
RUNNABLE | 執行狀態,呼叫start()方法後。在java執行緒中,將作業系統執行緒的就緒和執行統稱執行狀態 |
BLOCKED | 阻塞狀態,執行緒等待進入synchronized程式碼塊或方法中,等待獲取鎖 |
WAITING | 等待狀態,執行緒可呼叫wait、join等操作使自己陷入等待狀態,並等待其他執行緒做出特定操作(如notify或中斷) |
TIMED_WAITING | 超時等待,執行緒呼叫sleep(timeout)、wait(timeout)等操作進入超時等待狀態,超時後自行返回 |
TERMINATED | 終止狀態,執行緒執行結束 |
對於以上執行緒間的狀態及轉化關係,我們需要知道
WAITING(等待狀態)和TIMED_WAITING(超時等待)都會令執行緒進入等待狀態,不同的是TIMED_WAITING會在超時後自行返回,而WAITING則需要等待至條件改變。
進入阻塞狀態的唯一前提是在等待獲取同步鎖。java註釋說的很明白,只有兩種情況可以使執行緒進入阻塞狀態:一是等待進入synchronized塊或方法,另一個是在呼叫wait()方法後重新進入synchronized塊或方法。下文會有詳細解釋。
Lock類對於鎖的實現不會令執行緒進入阻塞狀態,Lock底層呼叫LockSupport.park()方法,使執行緒進入的是等待狀態。
wait/notify用例
讓我們先通過一個示例解析
synchronized,的主要機制使得鎖定到當前執行緒,其他執行緒需等待此執行緒釋放鎖,才得以訪問,而wait()方法可以使執行緒進入等待狀態,(暫時放棄此物件的鎖,使自己陷入等待佇列中),而notify()可以使等待的狀態喚醒(使得上一個利用wait陷入等待的執行緒繼續執行)。這樣的同步機制十分適合生產者、消費者模式:消費者消費某個資源,而生產者生產該資源。當該資源缺失時,消費者呼叫wait()方法進行自我阻塞,等待生產者的生產;生產者生產完畢後呼叫notify/notifyAll()喚醒消費者進行消費。
以下是程式碼示例,其中flag標誌表示資源的有無。
public class ThreadTest {
static final Object obj = new Object();
private static boolean flag = false;
public static void main(String[] args) throws Exception {
Thread consume = new Thread(new Consume(), "Consume");
Thread produce = new Thread(new Produce(), "Produce");
consume.start();
Thread.sleep(1000);
produce.start();
try {
produce.join();
consume.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生產者執行緒
static class Produce implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("進入生產者執行緒");
System.out.println("生產");
try {
TimeUnit.MILLISECONDS.sleep(2000); //模擬生產過程
flag = true;
obj.notify(); //通知消費者
TimeUnit.MILLISECONDS.sleep(1000); //模擬其他耗時操作
System.out.println("退出生產者執行緒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消費者執行緒
static class Consume implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("進入消費者執行緒");
System.out.println("wait flag 1:" + flag);
while (!flag) { //判斷條件是否滿足,若不滿足則等待
try {
System.out.println("還沒生產,進入等待");
obj.wait();
System.out.println("結束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait flag 2:" + flag);
System.out.println("消費");
System.out.println("退出消費者執行緒");
}
}
}
}
輸出結果為:
進入消費者執行緒
wait flag 1:false
還沒生產,進入等待
進入生產者執行緒
生產
退出生產者執行緒
結束等待
wait flag 2:true
消費
退出消費者執行緒
理解了輸出結果的順序,也就明白了wait/notify的基本用法。有以下幾點需要知道:
在示例中沒有體現但很重要的是,wait/notify方法的呼叫必須處在該物件的鎖(Monitor)中,也即,在呼叫這些方法時首先需要獲得該物件的鎖。否則會爬出IllegalMonitorStateException異常。
從輸出結果來看,在生產者呼叫notify()後,消費者並沒有立即被喚醒,而是等到生產者退出同步塊後才喚醒執行。(這點其實也好理解,synchronized同步方法(塊)同一時刻只允許一個執行緒在裡面,生產者不退出,消費者也進不去)
注意,消費者被喚醒後是從wait()方法(被阻塞的地方)後面執行,而不是重新從同步塊開頭。
深入瞭解
這一節我們探討wait/notify與執行緒狀態之間的關係。深入瞭解執行緒的生命週期。
由前面執行緒的狀態轉化圖可知,當呼叫wait()方法後,執行緒會進入WAITING(等待狀態),後續被notify()後,並沒有立即被執行,而是進入等待獲取鎖的阻塞佇列。
對於每個物件來說,都有自己的等待佇列和阻塞佇列。以前面的生產者、消費者為例,我們拿obj物件作為物件鎖,配合圖示。內部流程如下
當執行緒A(消費者)呼叫wait()方法後,執行緒A讓出鎖,自己進入等待狀態,同時加入鎖物件的等待佇列。
執行緒B(生產者)獲取鎖後,呼叫notify方法通知鎖物件的等待佇列,使得執行緒A從等待佇列進入阻塞佇列。
執行緒A進入阻塞佇列後,直至執行緒B釋放鎖後,執行緒A競爭得到鎖繼續從wait()方法後執行。