前言
線程是操作系統中的一個概念,支持多線程的語言都是對OS中的線程進行了封裝。要學好線程,就要搞清除它的生命周期,也就是生命周期各個節點的狀態轉換機制。不同的開發語言對操作系統中的線程進行了不同的封裝,但是對於線程的聲明周期這部分基本是相同的。下面先介紹通用的線程生命周期模型,然后詳細介紹Java中的線程生命周期以及Java生命周期中各個狀態是如何轉換的。
通用的線程生命周期
上圖為通用線程狀態轉換圖(五態模型)。
-
初始狀態
線程被創建,但是還不允許分配CPU執行。這里的創建僅僅是指在編程語言層面被創建;在OS層面還沒有被創建。
-
可運行狀態
線程可以分配CPU執行。在這種狀態下,真正的OS線程已經被成功創建,所以可以分配CPU執行。
-
運行狀態
當有空閑的CPU時,OS就會將空閑CPU分配給一個處於可運行狀態的線程,被分配到CPU的線程的狀態就轉換成了運行狀態。
-
休眠狀態
運行狀態的線程如果調用一個阻塞的API(例如以阻塞方式讀文件)或者等待某個事件(例如條件變量),那么線程的狀態就會轉到休眠狀態,此時會釋放CPU使用權,休眠狀態的線程永遠沒有機會獲得CPU的使用權。當等待的事件出現了(線程被喚醒),線程就會從休眠狀態轉到可運行狀態。
-
終止狀態
程序執行完成或者出現異常就會進入此狀態。終止狀態的線程不會切換到其他任何狀態,進入終止狀態也就意味着線程的生命周期結束了。
以上五種狀態在不同的編程語言中會簡化合並(C中POSIX Thread規范將初始狀態和可運行狀態合並)或者細化(Java中細化了休眠狀態)。Java中將可運行狀態和運行狀態合並了,Java虛擬機不關心這兩個狀態,把線程的調度交給了操作系統。
Java線程的生命周期
Java語言的線程共有六種狀態:New
(初始化狀態)、RUNNABLE
(可運行狀態/運行狀態)、BLOCKED
(阻塞狀態)、WAITING
(無時限等待)、TIMED_WAITING
(有時限等待)、TERMINATED
(終止狀態)。
在操作系統層面,Java線程中的 BLOCKED、 WAITING 、TIMED_WAITING都是休眠狀態。只要Java處於這三種狀態之一,那么這個線程就永遠沒有CPU使用權。
下面是Java線程的狀態轉換圖:
這六種狀態之間的轉換,注意箭頭的方向,哪些狀態是可以互轉的哪些是不可以互轉。
RUNNABLE ——> BLOCKED
只有一種場景會觸發這種轉換,即線程等待synchronized內置鎖。synchronized關鍵修飾的方法、代碼塊同一時刻只允許一個線程執行,其他未能執行的線程則等待。這種情況下,等待的線程就會從RUNNABLE轉換到 BLOCKED狀態。當等待的線程獲得內置鎖時,就會從BLOCKED轉換到RUNNABLE狀態。
線程調用阻塞式API時,在操作系統層面線程是會轉到休眠狀態,但是在Java虛擬機層面,Java線程的狀態是不會發生變化的,會保持RUNNABLE狀態。Java虛擬機層面並不關心操作系統相關調度狀態,在它眼里,等待CPU使用權(OS層面處於可執行狀態)和等待I/O(OS層面處於休眠狀態)沒有區別,都是在等待某個資源,所以都歸入了RUNNABLE狀態。
所以,平時說Java在調用阻塞式API時,線程會阻塞,指的是操作系統線程的狀態,並不是Java線程的狀態。
RUNNABLE ——> WAITING
有三種場景會觸發這種轉換:
-
獲取synchronized內置鎖的線程,調用無參數的
Object.wait()
方法。當前線程調用
wait()
方法會將自己阻塞,狀態就從從RUNNABLE轉到WAITING狀態。使用同一內置鎖的其他線程可調用notifyAll()
喚醒阻塞在該鎖上的所有線程,此時被阻塞的線程狀態就會從WAITING轉到RUNNABLE狀態。 -
調用
Thread.join()
方法。一個線程對象thread A,當調用
A.join()
的時候,執行這條語句的線程會等待thread A執行完,而等待的這個線程,其狀態就會就會從RUNNABLE轉到WAITING狀態。當thread A執行完,原來的這個等待線程就會從WAITING狀態轉到RUNNABLE狀態。 -
調用
LockSupport.park()
方法。Java並發包中的鎖都是基於
LockSupport
對象實現的。調用LockSupport.park()
的當前線程會被阻塞,線程的狀態會從RUNNABLE轉到WAITING狀態。調用LockSupport.unpark(Thread t)
可喚醒被阻塞的目標線程,目標線程的狀態就會從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)
方法。
較與WAITING狀態觸發條件多了超時參數。
NEW ——> RUNNALE,創建線程的兩種方式
Java剛創建出來的Thread對象就是NEW狀態,而創建Thread對象主要有兩種方法。
一種是繼承Thread對象,重寫run()方法。
// 自定義線程類
class MyThread extends Thread {
@Override
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狀態。那么如何轉到RUNNABLE狀態呢?那就需要線程啟動,即調用線程的start()
方法。
MyThread myThread = new MyThread();
// 從 NEW 狀態轉換到 RUNNABLE 狀態
myThread.start();
RUNNABLE——> TERMINATED
線程執行完 run()
方法后,會自動轉換到 TERMINATED 狀態。
如果執行 run() 方法的時候異常拋出,也會導致線程終止。有時候我們需要強制中斷 run() 方法的執行,可以發現Java 的 Thread 類里面倒是有個 stop()
方法,但是該方法被標記為 @Deprecated
,已經被棄用了。所以,正確的方式是調用 interrupt()
方法。
stop()方法和interrupt()方法的主要區別:
stop()方法會直接殺死線程。如果線程持有 ReentrantLock 鎖,被 stop() 的線程並不會自動調用 ReentrantLock 的 unlock() 去釋放鎖,那其他線程將再也沒機會獲得 ReentrantLock 鎖。這將會導致非常糟糕的結果。所以該方法已經被廢棄。
而 interrupt() 方法就比較溫柔,interrupt() 方法僅僅是通知線程,線程有機會執行一些后續操作,同時也可以無視這個通知。
線程是如何收到interrupt通知呢?有兩種方式,一種是異常,一種是主動檢測。
異常獲取通知:
當線程 A 處於 WAITING、TIMED_WAITING 狀態時,如果其他線程調用線程 A 的 interrupt() 方法,會使線程 A 返回到 RUNNABLE 狀態,同時線程 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 的 interrupt() 方法,那么線程 A 可以通過 isInterrupted() 方法,檢測是不是自己被中斷了。
小結
線程的生命周期以及各個狀態的轉換要好好掌握,這對於調試bug還是很有用的。
參考:
[1]極客時間專欄王寶令《Java並發編程實戰》