前言
在 Java 領域,實現並發程序的主要手段就是多線程。線程是操作系統里的一個概念,雖然各種不同的開發語言如 Java、C# 等都對其進行了封裝,但是萬變不離操作系統。Java 語言里的線程本質上就是操作系統的線程,它們是一一對應的。
在操作系統層面,線程也有“生老病死”,專業的說法叫有生命周期。對於有生命周期的事物,要學好它,思路非常簡單,只要能搞懂生命周期中各個節點的狀態轉換機制就可以了。
雖然不同的開發語言對於操作系統線程進行了不同的封裝,但是對於線程的生命周期這部分,基本上是雷同的。所以,我們可以先來了解一下通用的線程生命周期模型,這部分內容也適用於很多其他編程語言;然后再詳細有針對性地學習一下 Java 中線程的生命周期。
通用的線程生命周期
通用的線程生命周期基本上可以用下圖這個“五態模型”來描述。這五態分別是:初始狀態、可運行狀態、運行狀態、休眠狀態和終止狀態。
這“五態模型”的詳細情況如下所示。
初始態:指的是線程已經被創建,但是還不允許分配 CPU 執行。這個狀態屬於編程語言特有的,不過這里所謂的被創建,僅僅是在編程語言層面被創建,而在操作系統層面,真正的線程還沒有創建。
可運行態: 指的是線程可以分配 CPU 執行。在這種狀態下,真正的操作系統線程已經被成功創建了,所以可以分配 CPU 執行。
運行態:當有空閑的 CPU 時,操作系統會將其分配給一個處於可運行狀態的線程,被分配到 CPU 的線程的狀態就轉換成了運行狀態。
休眠態:運行狀態的線程如果調用一個阻塞的 API(例如以阻塞方式讀文件)或者等待某個事件(例如條件變量),那么線程的狀態就會轉換到休眠狀態,同時釋放 CPU 使用權,休眠狀態的線程永遠沒有機會獲得 CPU 使用權。當等待的事件出現了,線程就會從休眠狀態轉換到可運行狀態。
終止態:線程執行完或者出現異常就會進入終止狀態,終止狀態的線程不會切換到其他任何狀態,進入終止狀態也就意味着線程的生命周期結束了。
這五種狀態在不同編程語言里會有簡化合並。例如,C 語言的 POSIX Threads 規范,就把初始狀態和可運行狀態合並了;Java 語言里則把可運行狀態和運行狀態合並了,這兩個狀態在操作系統調度層面有用,而 JVM 層面不關心這兩個狀態,因為 JVM 把線程調度交給操作系統處理了。
Java 中線程的生命周期
介紹完通用的線程生命周期模型,想必你已經對線程的“生老病死”有了一個大致的了解。那接下來我們就來詳細看看 Java 語言里的線程生命周期是什么樣的。
Java 語言中線程共有六種狀態,分別是:
NEW(初始化狀態)
RUNNABLE(可運行 / 運行狀態)
BLOCKED(阻塞狀態)
WAITING(無時限等待)
TIMED_WAITING(有時限等待)
TERMINATED(終止狀態)
這看上去挺復雜的,狀態類型也比較多。但其實在操作系統層面,Java 線程中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即前面我們提到的休眠狀態。也就是說只要 Java 線程處於這三種狀態之一,那么這個線程就永遠沒有 CPU 的使用權。所以 Java 線程的生命周期可以簡化為下圖:
其中,BLOCKED、WAITING、TIMED_WAITING 可以理解為線程導致休眠狀態的三種原因。那具體是哪些情形會導致線程從 RUNNABLE 狀態轉換到這三種狀態呢?而這三種狀態又是何時轉換回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 狀態是如何轉換的?
JAVA中線程狀態轉換
RUNNABLE 與 BLOCKED 的狀態轉換
只有一種場景會觸發這種轉換,就是線程等待 synchronized 的隱式鎖。synchronized 修飾的方法、代碼塊同一時刻只允許一個線程執行,其他線程只能等待,這種情況下,等待的線程就會從 RUNNABLE 轉換到 BLOCKED 狀態。而當等待的線程獲得 synchronized 隱式鎖時,就又會從 BLOCKED 轉換到 RUNNABLE 狀態。如果你熟悉操作系統線程的生命周期的話,可能會有個疑問:線程調用阻塞式 API 時,是否會轉換到 BLOCKED 狀態呢?
在操作系統層面,線程是會轉換到休眠狀態的,但是在 JVM 層面,Java 線程的狀態不會發生變化,也就是說 Java 線程的狀態會依然保持 RUNNABLE 狀態。JVM 層面並不關心操作系統調度相關的狀態,因為在 JVM 看來,等待 CPU 使用權(操作系統層面此時處於可執行狀態)與等待 I/O(操作系統層面此時處於休眠狀態)沒有區別,都是在等待某個資源,所以都歸入了 RUNNABLE 狀態。而我們平時所謂的 Java 在調用阻塞式 API 時,線程會阻塞,指的是操作系統線程的狀態,並不是 Java 線程的狀態。
RUNNABLE 與 WAITING 的狀態轉換
總體來說,有三種場景會觸發這種轉換。
- 獲得 synchronized 隱式鎖的線程調用了無參數的 Object.wait() 方法。其中,wait() 方法我們在上一篇講解管程的時候已經深入介紹過了,這里就不再贅述。
- 調用無參數的 Thread.join() 方法。其中的 join() 是一種線程同步方法,例如有一個線程對象 thread A,當調用 A.join() 的時候,執行這條語句的線程會等待 thread A 執行完,而等待中的這個線程,其狀態會從 RUNNABLE 轉換到 WAITING。當線程 thread A 執行完,原來等待它的線程又會從 WAITING 狀態轉換到 RUNNABLE。
- 調用 LockSupport.park() 方法。其中的 LockSupport 對象,也許你有點陌生,其實 Java 並發包中的鎖,都是基於它實現的。調用 LockSupport.park() 方法,當前線程會阻塞,線程的狀態會從 RUNNABLE 轉換到 WAITING。調用 LockSupport.unpark(Thread thread) 可喚醒目標線程,目標線程的狀態又會從 WAITING 狀態轉換到 RUNNABLE。
RUNNABLE 與 TIMED_WAITING 的狀態轉換
有五種場景會觸發這種轉換:
- 調用帶超時參數的 Thread.sleep(long millis) 方法;
- 獲得 synchronized 隱式鎖的線程,調用帶超時參數的 Object.wait(long timeout) 方法;
- 調用帶超時參數的 Thread.join(long millis) 方法;
- 調用帶超時參數的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
- 調用帶超時參數的 LockSupport.parkUntil(long deadline) 方法。
- 這里你會發現 TIMED_WAITING 和 WAITING 狀態的區別,僅僅是觸發條件多了超時參數。
從 NEW 到 RUNNABLE 狀態
Java 剛創建出來的 Thread 對象就是 NEW 狀態,而創建 Thread 對象主要有兩種方法。一種是繼承 Thread 對象,重寫 run() 方法。示例代碼如下:
// 自定義線程對象
class MyThread extends Thread {
public void run() {
// 線程需要執行的代碼
......
}
}
// 創建線程對象
MyThread myThread = new MyThread();
另一種是實現 Runnable 接口,重寫 run() 方法,並將該實現類作為創建 Thread 對象的參數。示例代碼如下:
// 實現 Runnable 接口
class Runner implements Runnable {
@Override
public void run() {
// 線程需要執行的代碼
......
}
}
// 創建線程對象
Thread thread = new Thread(new Runner());
NEW 狀態的線程,不會被操作系統調度,因此不會執行。Java 線程要執行,就必須轉換到 RUNNABLE 狀態。從 NEW 狀態轉換到 RUNNABLE 狀態很簡單,只要調用線程對象的 start() 方法就可以了,示例代碼如下:
MyThread myThread = new MyThread();
// 從 NEW 狀態轉換到 RUNNABLE 狀態
myThread.start();
從 RUNNABLE 到 TERMINATED 狀態
線程執行完 run() 方法后,會自動轉換到 TERMINATED 狀態,當然如果執行 run() 方法的時候異常拋出,也會導致線程終止。有時候我們需要強制中斷 run() 方法的執行,例如 run() 方法訪問一個很慢的網絡,我們等不下去了,想終止怎么辦呢?Java 的 Thread 類里面倒是有個 stop() 方法,不過已經標記為 @Deprecated,所以不建議使用了。正確的姿勢其實是調用 interrupt() 方法。
stop() 和 interrupt() 方法的主要區別是什么呢?
stop() 方法會真的殺死線程,不給線程喘息的機會,如果線程持有 ReentrantLock 鎖,被 stop() 的線程並不會自動調用 ReentrantLock 的 unlock() 去釋放鎖,那其他線程就再也沒機會獲得 ReentrantLock 鎖,這實在是太危險了。所以該方法就不建議使用了,類似的方法還有 suspend() 和 resume() 方法,這兩個方法同樣也都不建議使用了,所以這里也就不多介紹了。
而 interrupt() 方法就溫柔多了,interrupt() 方法僅僅是通知線程,線程有機會執行一些后續操作,同時也可以無視這個通知。被 interrupt 的線程,是怎么收到通知的呢?一種是異常,另一種是主動檢測。
只有當線程 A 處於 WAITING、TIMED_WAITING 狀態時,如果其他線程調用線程 A 的 interrupt() 方法,會使線程 A 返回到 RUNNABLE 狀態(中間可能存在BLOCKED狀態),同時線程 A 的代碼會觸發 InterruptedException 異常。上面我們提到轉換到 WAITING、TIMED_WAITING 狀態的觸發條件,都是調用了類似 wait()、join()、sleep() 這樣的方法,我們看這些方法的簽名,發現都會 throws InterruptedException 這個異常。這個異常的觸發條件就是:其他線程調用了該線程的 interrupt() 方法。
當線程 A 處於 RUNNABLE 狀態時,並且阻塞在 java.nio.channels.InterruptibleChannel 上時,如果其他線程調用線程 A 的 interrupt() 方法,線程 A 會觸發 java.nio.channels.ClosedByInterruptException 這個異常;而阻塞在 java.nio.channels.Selector 上時,如果其他線程調用線程 A 的 interrupt() 方法,線程 A 的 java.nio.channels.Selector 會立即返回。
上面這兩種情況屬於被中斷的線程通過異常的方式獲得了通知。還有一種是主動檢測,如果線程處於 RUNNABLE 狀態,並且沒有阻塞在某個 I/O 操作上,例如中斷計算圓周率的線程 A,這時就得依賴線程 A 主動檢測中斷狀態了。如果其他線程調用線程 A 的 interrupt() 方法,那么線程 A 可以通過 isInterrupted() 方法,檢測是不是自己被中斷了。
總結
理解 Java 線程的各種狀態以及生命周期對於診斷多線程 Bug 非常有幫助,多線程程序很難調試,出了 Bug 基本上都是靠日志,靠線程 dump 來跟蹤問題,分析線程 dump 的一個基本功就是分析線程狀態,大部分的死鎖、飢餓、活鎖問題都需要跟蹤分析線程的狀態。同時,本文介紹的線程生命周期具備很強的通用性,對於學習其他語言的多線程編程也有很大的幫助。你可以通過 jstack命令或者Java VisualVM。這個可視化工具將 JVM 所有的線程棧信息導出來,完整的線程棧信息不僅包括線程的當前狀態、調用棧,還包括了鎖的信息。例如,我曾經寫過一個死鎖的程序,導出的線程棧明確告訴我發生了死鎖,並且將死鎖線程的調用棧信息清晰地顯示出來了(如下圖)。導出線程棧,分析線程狀態是診斷並發問題的一個重要工具。