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 | 終止狀态,線程運作結束 |
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISMvwlcvJncl1SZy9mbnl2LcBnYld3LcRXYtJ3bm9CXwYTOvwFavwFM4ITMvw1dvwFMvwlM3VWaWV2Zh1Wa-kTNkVDO0kDN4gDZilTO2EzLcFjMvw1MvwVOxAjMvw1bp5Sd0lGeu4GZj1CZs92ZtIXZzV3Lc9CX6MHc0RHaiojIsJye.jpg)
對于以上線程間的狀态及轉化關系,我們需要知道
- WAITING(等待狀态)和TIMED_WAITING(逾時等待)都會令線程進入等待狀态,不同的是TIMED_WAITING會在逾時後自行傳回,而WAITING則需要等待至條件改變。
- 進入阻塞狀态的唯一前提是在等待擷取同步鎖。java注釋說的很明白,隻有兩種情況可以使線程進入阻塞狀态:一是等待進入synchronized塊或方法,另一個是在調用wait()方法後重新進入synchronized塊或方法。下文會有詳細解釋。
- Lock類對于鎖的實作不會令線程進入阻塞狀态,Lock底層調用LockSupport.park()方法,使線程進入的是等待狀态。
wait/notify用例
讓我們先通過一個示例解析
wait()方法可以使線程進入等待狀态,而notify()可以使等待的狀态喚醒。這樣的同步機制十分适合生産者、消費者模式:消費者消費某個資源,而生産者生産該資源。當該資源缺失時,消費者調用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()方法後執行。