1. 程式人生 > >【Java併發基礎】Java執行緒的生命週期

【Java併發基礎】Java執行緒的生命週期

前言

執行緒是作業系統中的一個概念,支援多執行緒的語言都是對OS中的執行緒進行了封裝。要學好執行緒,就要搞清除它的生命週期,也就是生命週期各個節點的狀態轉換機制。不同的開發語言對作業系統中的執行緒進行了不同的封裝,但是對於執行緒的宣告週期這部分基本是相同的。下面先介紹通用的執行緒生命週期模型,然後詳細介紹Java中的執行緒生命週期以及Java生命週期中各個狀態是如何轉換的。

通用的執行緒生命週期

上圖為通用執行緒狀態轉換圖(五態模型)。

  1. 初始狀態

    執行緒被建立,但是還不允許分配CPU執行。這裡的建立僅僅是指在程式語言層面被建立;在OS層面還沒有被建立。

  2. 可執行狀態

    執行緒可以分配CPU執行。在這種狀態下,真正的OS執行緒已經被成功建立,所以可以分配CPU執行。

  3. 執行狀態

    當有空閒的CPU時,OS就會將空閒CPU分配給一個處於可執行狀態的執行緒,被分配到CPU的執行緒的狀態就轉換成了執行狀態。

  4. 休眠狀態

    執行狀態的執行緒如果呼叫一個阻塞的API(例如以阻塞方式讀檔案)或者等待某個事件(例如條件變數),那麼執行緒的狀態就會轉到休眠狀態,此時會釋放CPU使用權,休眠狀態的執行緒永遠沒有機會獲得CPU的使用權。當等待的事件出現了(執行緒被喚醒),執行緒就會從休眠狀態轉到可執行狀態。

  5. 終止狀態

    程式執行完成或者出現異常就會進入此狀態。終止狀態的執行緒不會切換到其他任何狀態,進入終止狀態也就意味著執行緒的生命週期結束了。

以上五種狀態在不同的程式語言中會簡化合並(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

有三種場景會觸發這種轉換:

  1. 獲取synchronized內建鎖的執行緒,呼叫無引數的Object.wait()方法。

    當前執行緒呼叫wait()方法會將自己阻塞,狀態就從從RUNNABLE轉到WAITING狀態。使用同一內建鎖的其他執行緒可呼叫notifyAll()喚醒阻塞在該鎖上的所有執行緒,此時被阻塞的執行緒狀態就會從WAITING轉到RUNNABLE狀態。

  2. 呼叫Thread.join()方法。

    一個執行緒物件thread A,當呼叫A.join()的時候,執行這條語句的執行緒會等待thread A執行完,而等待的這個執行緒,其狀態就會就會從RUNNABLE轉到WAITING狀態。當thread A執行完,原來的這個等待執行緒就會從WAITING狀態轉到RUNNABLE狀態。

  3. 呼叫LockSupport.park()方法。

    Java併發包中的鎖都是基於LockSupport物件實現的。呼叫LockSupport.park()的當前執行緒會被阻塞,執行緒的狀態會從RUNNABLE轉到WAITING狀態。呼叫LockSupport.unpark(Thread t)可喚醒被阻塞的目標執行緒,目標執行緒的狀態就會從WAITING轉到RUNNABLE狀態。

RUNNABLE ——> TIMED_WAITING

以下場景將會觸發這個狀態轉變:

  1. 呼叫帶超時引數的Thread.sleep(long millis)方法。
  2. 獲得 synchronized 內建鎖的執行緒,呼叫帶超時引數的 Object.wait(long timeout)方法;
  3. 呼叫帶超時引數的 Thread.join(long millis)方法;
  4. 呼叫帶超時引數的 LockSupport.parkNanos(Object blocker, long deadline)方法;
  5. 呼叫帶超時引數的 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併發程式設計實戰