09|Java線程(上):Java線程的生命周期

在Java 領域,實現并發程序的主要手段就是多線程,線程是操作系統里的一個概念,雖然各種語言如 Java、C# 等都對其進行了封裝,但是萬變不離操作系統。 Java 語言里的線程本質上就是操作系統的線程,他們是一一對應的。

在操作系統層面,線程也有生老病死,專業的說法叫生命周期,對于有生命周期的事物,要學好它 思路非常簡單,只要能搞懂生命周期中各個節點的狀態轉化機制就可以了。

雖然不同的語言對于操作系統線程進行了不同的封裝,但是對于線程的生命周期這部分,基本上是雷同的。所以我們可以先來了解下通用的線程聲明周期。然后再詳細有針對性的學習一下Java 中線程的聲明周期。

通用的線程生命周期

通用的線程生命周期可以用下圖 這個 五態模型來表示,這五態分別是:初始狀態、可運行狀態、運行狀態、休眠狀態、終止狀態。

img

這 五態詳細情況如下:

1、初始狀態:指的是線程已經被創建,但是還不允許分配CPU執行。這個狀態屬于編程語言特有的,不過這里所謂的被創建,僅僅是在編程語言層面被創建,而在操作系統層面,真正的線程還沒有被創建。

2、可運行狀態:指的是線程可以分配CPU執行。在這種狀態下,真正的操作系統線程已經被創建了,所以可以分配CPU,執行。

3、當有空閑的CPU 時,操作系統會將其分配給一個處于可運行狀態的線程,被分配到CPU 的線程狀態就轉化為運行狀態。

4、運行狀態的線程如果調用了一個阻塞的API(例如以阻塞方式讀文件)或者等待某個事件(例如條件變量)那么線程的狀態就轉到休眠狀態,同時釋放CPU的使用權,休眠狀態的線程永遠沒有機會獲得CPU的使用權,當等待的事件出現了,線程就會從休眠狀態轉換到可運行狀態。

5、線程運行完或者出現異常就會進入終止狀態,終止狀態下的線程不會切換到其他任何狀態,進入終止狀態就意味著線程的聲明周期結束了。

這五種狀態在不同的編程語言會有簡化合并。例如,C 語言的 POSIX Threads 規范,就把初始狀態和可運行狀態合并了; Java 語言里則把可運行狀態和運行狀態合并了,這兩個狀態在操作系統調度層面有用,而 JVM 層面不關心這兩個狀態,因為 JVM 把線程調度交給操作系統處理了。

Java 中線程的聲明周期

介紹完通用的線程生命周期模型,想必你已經對線程的生老病死有了一個大致的了解,那接下來我們就看看Java 語言里的生命周期是什么樣的。

Java 語言中線程有六種狀態,分別是:

  1. NEW 初始狀態
  2. RUNNABLE 可運行狀態/運行狀態
  3. BLOCKED 阻塞狀態
  4. WAITING 無時限等待
  5. TIMED_WAITING 有時限等待
  6. TERMINATED 終止狀態

這看上去挺復雜,狀態類型也比較多,但其實在操作系統層面,Java 線程中的 BLOCKED、WAITING、TIMED_WAITING 是一種狀態,即前面我們提到的休眠狀態。 也就是說,只要Java 線程處于這三種狀態之一,那么這個線程就永遠沒有CPU 的使用權。

所以Java 線程的生命周期可以簡化為:

img

其中,BLOCKED、WAITING、TIMED_WAITING 可以理解為線程導致休眠狀態的三種原因。 那具體是哪些情形會導致線程從RUNNABLE 轉化到這三種狀態呢?而這三種狀態又是何時轉換回 RUNNABLE 的呢?以及 NEW、TERMINATED 和 RUNNABLE 狀態是如何轉換的?

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 的狀態轉換

有五種場景觸發這種轉換:

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) 方法。

這里你會發現,TIMED_WAITING 和 WAITING 狀態的區別,僅僅是觸發條件多了超時參數。

從 NEW 到 RUNNABLE 狀態

Java 剛創建出來的Thread 就是NEW 狀態的,而創建Thread 對象主要有兩種方法。

一種是繼承 Thread 對象,重寫 run() 方法。

另一種是實現 Runnable 接口,重寫 run() 方法,并將該實現類作為創建 Thread 對象的參數。

NEW 狀態的線程,不會被操作系統調度,因此不會被執行。Java 線程要執行,就必須轉換到RUNNABLE 狀態,從 NEW 狀態轉換到 RUNNABLE 狀態很簡單,只要調用線程對象的 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 狀態,同時線程 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 的一個基本功就是分析線程狀態,大部分的死鎖、饑餓、活鎖問題都需要跟蹤分析線程的狀態。同時,本文介紹的線程生命周期具備很強的通用性,對于學習其他語言的多線程編程也有很大的幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容