目錄
一、線程的五種基本狀態
二、線程從 RUNNABLE 進入阻塞 / 等待狀態的三種典型場景
1. 調用sleep(long millis):進入 TIMED_WAITING 狀態
2. 調用wait():進入 WAITING/TIMED_WAITING 狀態
3. 等待 I/O 資源或獲取鎖失敗:進入 BLOCKED 狀態
三、線程從阻塞 / 等待狀態回到 RUNNABLE 狀態的底層原理
1. TIMED_WAITING 狀態的喚醒(如sleep()超時)
2. WAITING/TIMED_WAITING 狀態的喚醒(如wait()被喚醒)
對象監視器(Monitor)
wait()方法的底層執行流程
notify()/notifyAll()的底層執行流程
3. BLOCKED 狀態的喚醒(如獲取鎖或 I/O 就緒)
四、總結:線程狀態轉換的核心邏輯
在多線程編程中,線程狀態的轉換是理解并發行為的核心。當運行中的線程因調用sleep()
、wait()
或等待 I/O 等操作讓出 CPU 時,會進入特定的阻塞狀態;而從阻塞狀態重新回到可運行狀態的過程,更是體現了 JVM 對線程調度的精妙設計。本文將深入解析線程狀態轉換的底層機制,揭示線程如何在不同狀態間切換。
一、線程的五種基本狀態
根據 Java 線程模型,線程的生命周期包含五種狀態,由Thread.State
枚舉定義:
- 新建(NEW):線程對象已創建但未調用
start()
方法。 - 可運行(RUNNABLE):線程已啟動,正在 JVM 中運行或等待 CPU 調度。
- 阻塞(BLOCKED):線程等待獲取 synchronized 鎖(進入 synchronized 塊 / 方法時未搶到鎖)。
- 等待(WAITING):線程無限期等待被其他線程喚醒(如調用
wait()
、join()
無參方法)。 - 超時等待(TIMED_WAITING):線程在指定時間內等待(如
sleep(1000)
、wait(1000)
)。 - 終止(TERMINATED):線程執行完畢或因異常退出。
核心轉換場景:運行狀態(RUNNABLE)的線程會因特定操作進入阻塞 / 等待狀態,待條件滿足后重新回到 RUNNABLE 狀態。
二、線程從 RUNNABLE 進入阻塞 / 等待狀態的三種典型場景
運行中的線程(RUNNABLE)在以下情況會讓出 CPU,進入非運行狀態:
1. 調用sleep(long millis)
:進入 TIMED_WAITING 狀態
sleep()
是 Thread 類的靜態方法,作用是讓當前線程暫停執行指定毫秒數。調用后線程狀態變化為:
RUNNABLE → TIMED_WAITING
- 特點:
- 線程不會釋放已持有的鎖(如 synchronized 鎖)。
- 暫停時間是相對精確的(受系統時鐘和 CPU 調度影響)。
- 常用于模擬延遲(如網絡請求等待)。
public class SleepDemo {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {try {System.out.println("線程進入睡眠");Thread.sleep(3000); // 進入TIMED_WAITING狀態System.out.println("線程喚醒");} catch (InterruptedException e) {e.printStackTrace();}});t.start();}
}
2. 調用wait()
:進入 WAITING/TIMED_WAITING 狀態
wait()
是 Object 類的方法,必須在synchronized
塊中調用,作用是讓線程釋放鎖并等待被喚醒。調用后狀態變化為:
RUNNABLE → WAITING(無參wait()
)或TIMED_WAITING(wait(long timeout)
)
- 特點:
- 必須持有對象鎖(synchronized),調用后立即釋放鎖。
- 需其他線程調用同一對象的
notify()
/notifyAll()
喚醒(或超時自動喚醒)。 - 常用于線程間協作(如生產者消費者模型)。
public class WaitDemo {private static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (lock) {try {System.out.println("線程進入等待");lock.wait(); // 進入WAITING狀態System.out.println("線程被喚醒");} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}
3. 等待 I/O 資源或獲取鎖失敗:進入 BLOCKED 狀態
當線程等待外部資源(如磁盤 IO、網絡請求)或嘗試獲取 synchronized 鎖但失敗時,會進入 BLOCKED 狀態:
RUNNABLE → BLOCKED
- 特點:
- 等待 I/O 時,線程暫停執行,直到資源就緒(如數據讀取完成)。
- 獲取鎖失敗時,線程進入鎖的 "入口集"(Entry Set)排隊,直到鎖被釋放。
public class BlockedDemo {private static final Object lock = new Object();public static void main(String[] args) {// 線程1先獲取鎖new Thread(() -> {synchronized (lock) {try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }}}).start();// 線程2后啟動,嘗試獲取鎖失敗,進入BLOCKED狀態new Thread(() -> {synchronized (lock) { // 此處會阻塞System.out.println("線程2獲取到鎖");}}).start();}
}
三、線程從阻塞 / 等待狀態回到 RUNNABLE 狀態的底層原理
線程進入阻塞 / 等待狀態后,并非永久停止,而是在特定條件滿足后重新回到 RUNNABLE 狀態。不同狀態的喚醒機制底層原理不同:
1. TIMED_WAITING 狀態的喚醒(如sleep()
超時)
sleep(long millis)
的喚醒依賴系統定時器:
-
底層流程:
- 線程調用
sleep()
時,JVM 向操作系統注冊一個定時事件(指定 millis 后觸發)。 - 線程從 RUNNABLE 狀態切換到 TIMED_WAITING,操作系統將其從 CPU 調度隊列中移除。
- 定時時間到達后,操作系統觸發事件,JVM 將線程重新放入 CPU 調度隊列,狀態切換為 RUNNABLE。
- 線程等待 CPU 調度,獲取時間片后繼續執行。
- 線程調用
-
特殊情況:若線程在
sleep()
期間被中斷(調用interrupt()
),會拋出InterruptedException
并提前喚醒,狀態直接回到 RUNNABLE。
2. WAITING/TIMED_WAITING 狀態的喚醒(如wait()
被喚醒)
要理解wait()
和notify()
/notifyAll()
的底層原理,需要從線程狀態轉換和對象監視器(Monitor)?兩個核心角度展開。這組方法是 Java 中實現線程間協作的基礎,其底層依賴于 JVM 對對象鎖的管理機制。
對象監視器(Monitor)
Java 中每個對象都隱式關聯一個對象監視器(Monitor),也稱為 “內置鎖” 或 “管程”。它是實現同步和線程協作的核心數據結構,內部包含三個關鍵部分:
- 持有線程:當前獲取到鎖的線程(同一時間只能有一個線程持有)。
- 入口集(Entry Set):等待獲取鎖的線程集合(線程狀態為
BLOCKED
)。 - 等待集(Wait Set):調用
wait()
后釋放鎖并等待被喚醒的線程集合(線程狀態為WAITING
或TIMED_WAITING
)。
簡單說,Monitor 就像一個 “休息室 + 會議室”:
- 會議室(鎖)同一時間只能有一個線程使用(持有鎖的線程);
- 入口集是等待進入會議室的線程隊列;
- 等待集是在會議室內主動暫停(調用
wait()
)并臨時退出到休息室的線程隊列。
wait()
方法的底層執行流程
當線程調用obj.wait()
時,底層會執行以下操作(必須在synchronized
塊中調用,否則會拋IllegalMonitorStateException
):
-
釋放當前對象的鎖
調用wait()
的線程必須已經持有obj
的鎖(即處于synchronized(obj)
塊中)。執行wait()
后,線程會主動釋放該鎖,退出 “持有線程” 位置,讓其他線程有機會獲取鎖。 -
進入對象的等待集(Wait Set)
線程釋放鎖后,會從 “運行狀態” 轉入 “等待狀態(WAITING)”,并被放入obj
的等待集(休息室)中。此時線程不再參與 CPU 調度,處于阻塞狀態。 -
等待被喚醒
線程在等待集中休眠,直到其他線程調用obj.notify()
或obj.notifyAll()
,才有可能被喚醒。若調用的是wait(long timeout)
,則超時后也會自動喚醒。 -
重新競爭鎖
被喚醒的線程不會立即執行,而是從等待集轉移到入口集(Entry Set),重新參與鎖的競爭。只有重新獲取到obj
的鎖后,才能從wait()
方法返回,繼續執行后續代碼。
-
關鍵細節:被喚醒的線程必須重新競爭鎖,因此
wait()
通常配合while
循環檢查條件(防止虛假喚醒):synchronized (obj) {while (條件不滿足) { // 用while而非if,避免虛假喚醒obj.wait();} }
notify()
/notifyAll()
的底層執行流程
notify()
和notifyAll()
用于喚醒等待集中的線程,底層流程如下:
notify()
:從對象的等待集中隨機選擇一個線程,將其從等待集移到入口集(Entry Set),使其有機會重新競爭鎖。notifyAll()
:將對象等待集中的所有線程全部移到入口集,讓所有等待線程重新競爭鎖。
注意:
- 調用
notify()
/notifyAll()
的線程不會立即釋放鎖,而是要等當前synchronized
塊執行完畢后才釋放。因此,被喚醒的線程需要等待喚醒它的線程釋放鎖后,才能重新競爭鎖。 - 若等待集中沒有線程,
notify()
/notifyAll()
調用無效果。
3. BLOCKED 狀態的喚醒(如獲取鎖或 I/O 就緒)
-
獲取鎖的 BLOCKED 線程:
- 線程因未搶到 synchronized 鎖進入 BLOCKED 狀態,排隊在鎖的入口集(Entry Set)。
- 當持有鎖的線程釋放鎖(退出 synchronized 塊),JVM 從入口集中喚醒一個或多個線程(具體策略由 JVM 實現決定)。
- 被喚醒的線程競爭鎖,成功后狀態切換為 RUNNABLE。
-
等待 I/O 的 BLOCKED 線程:
- 線程發起 I/O 請求后,進入 BLOCKED 狀態(如讀取文件時等待磁盤數據)。
- 操作系統負責處理 I/O 操作,完成后向 JVM 發送I/O 完成事件。
- JVM 接收到事件后,將線程狀態切換為 RUNNABLE,等待 CPU 調度。
四、總結:線程狀態轉換的核心邏輯
線程狀態轉換的本質是JVM 與操作系統協作的資源調度過程:
- 主動讓出 CPU:
sleep()
、wait()
等方法讓線程暫時放棄執行權,進入特定狀態。 - 阻塞原因解除:超時、被喚醒、資源就緒等條件觸發時,線程重新獲得執行資格。
- 競爭 CPU 時間片:所有回到 RUNNABLE 狀態的線程,最終需通過操作系統的 CPU 調度獲得執行機會。