【Java併發基礎】Java執行緒的生命週期
前言
執行緒是作業系統中的一個概念,支援多執行緒的語言都是對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併發程式設計實戰