面試官:都說阻塞 I/O 模型將會使線程休眠,為什麼 Java 線程狀態卻是 RUNNABLE?

  • 2019 年 10 月 3 日
  • 筆記

摘要: 原創出處 https://studyidea.cn 「公眾號:程序通事 」歡迎關注和轉載,保留摘要,謝謝!

使用 Java 阻塞 I/O 模型讀取數據,將會導致線程阻塞,線程將會進入休眠,從而讓出 CPU 的執行權,直到數據讀取完成。這個期間如果使用 jstack 查看線程狀態,卻可以發現Java 線程狀態是處於 RUNNABLE,這就和上面說的存在矛盾,為什麼會這樣?

上面的矛盾其實是混淆了操作系統線程狀態與 Java 線程狀態。這裡說的線程阻塞進入休眠狀態,其實是操作系統層麵線程實際狀態。而我們使用 jstack 查看的線程狀態卻是 JVM 中的線程狀態。

線程是操作系統中一種概念,Java 對其進行了封裝,Java 線程本質上就是操作系統的中線程,其狀態與操作系統的狀態大致相同,但還是存在一些區別。

下面首先來看我們熟悉的 Java 線程狀態。

Java 線程狀態

Java 線程狀態定義在 Thread.State 枚舉中,使用 thread#getState 方法可以獲取當前線程的狀態。

Thread.State 狀態如下圖

State.png

可以看到 Java 線程總共存在 6 中狀態,分別為:

  • NEW(初始狀態)
  • RUNNABLE(運行狀態)
  • BLOCKED(阻塞狀態)
  • WATTING(等待狀態)
  • TIMED_WAITING(限時等待狀態)
  • TERMINATED(終止狀態)

NEW(初始狀態)與 RUNNABLE(運行狀態)

每個使用 new Thread() 剛創建出線程實例狀態處於 NEW 狀態,一旦調用 thread.start(),線程狀態將會變成 RUNNABLE

RUNNABLE(運行狀態) 與 BLOCKED(阻塞狀態)

RUNNABLE 狀態的線程在進入由 synchronized修飾的方法或代碼塊前將會嘗試獲取一把隱式的排他鎖,一旦獲取不到,線程狀態將會變成 BLOCKED,等待獲取鎖。一旦有其他線程釋放這把鎖,線程成功搶到該鎖,線程狀態就將會從 BLOCKED 轉變為 RUNNABLE 狀態。

RUNNABLE(運行狀態) 與 WATTING(等待狀態)

處於 WATTING 狀態的線程將會一直處於無限期的等待狀態,需要等待其他線程喚醒。總共存在三種方法將會使線程從 RUNNABLE 變成 WATTING

  1. Object#wait

線程在獲取到 synchronized 隱式鎖後,顯示的調用 Object#wait()方法。這種情況下該線程將會讓出隱式鎖,一旦其他線程獲取到該鎖,且調用了 Object.notify()object.notifyAll(),線程將會喚醒,然後變成 RUNNABLE

  1. Thread#join

join方法是一種線程同步方法。假設我們在 main 方法中執行 Thread A.join() 方法,main 線程狀態就會變成 WATTING。直到 A 線程執行完畢,main 線程才會再變成 RUNNABLE

  1. LockSupport#park()

LockSupport 是 JDK 並發包里重要對象,很多鎖的實現都依靠該對象。一旦調用 LockSupport#park(),線程就將會變為 WATTING 狀態。如果需要喚醒線程就需要調用 LockSupport#unpark,然後線程狀態重新變為 RUNNABLE

RUNNABLE(運行狀態) 與 TIMED_WAITING(限時等待狀態)

TIMED_WAITINGWATTING 功能一樣,只不過前者增加限時等待的功能,一旦等待時間超時,線程狀態自動變為 RUNNABLE。以下幾種情況將會觸發這種狀態:

  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)

RUNNABLE(運行狀態)與 TERMINATED(終止狀態)

線程一旦執行結束或者線程執行過程發生異常且未正常捕獲處理,狀態都將會自動變成 TERMINATED

Java 線程 6 種狀態看起來挺複雜的,但其實上面 BLOCKEDWATTINGTIMED_WAITING,都會使線程處於休眠狀態,所以我們將這三類都歸類為休眠狀態。這麼分類的話,Java 線程生命周期就可以簡化為下圖:

java線程狀態2.png

通用操作系統線程狀態

上面講完 Java 系統的線程狀態,我們來看下通用操作系統的線程狀態。操作系統線程狀態可以分為初始狀態,可運行狀態,運行狀態,休眠狀態以及終止狀態,如下圖:

操作系統線程狀態1.png

這 5 中狀態詳細情況如下:

  1. 初始狀態,這時候線程剛被創建,還不能分配 CPU 。
  2. 可運行狀態,線程等待系統分配 CPU ,從而執行任務。
  3. 運行狀態,操作系統將 CPU 分配給線程,線程執行任務。
  4. 休眠狀態,運行狀態下的線程如果調用阻塞 API,如阻塞方式讀取文件, 線程狀態就將變成休眠狀態。這種情況下,線程將會讓出 CPU 使用權。休眠結束,線程狀態將會先變成可運行狀態。
  5. 線程執行結束或者執行過程發生異常將會使線程進入終止狀態,這個狀態下線程使命已經結束。

對比兩者線程狀態

比較 Java 線程與操作系統線程,可以發現 Java 線程狀態沒有可運行狀態。也就是說 Java 線程 RUNNABLE 狀態包括了操作系統的可運行狀態與運行狀態。一個處於 RUNNABLE 狀態 Java 線程,在操作系統層面狀態可能為可運行狀態,正在等待系統分配 CPU 使用權。

另外 Java 線程細分了操作系統休眠狀態,分成了 BLOCKEDWATTINGTIMED_WAITING 三種。

當線程調用阻塞式 API,線程進入休眠狀態,這裡指的是操作系統層面的。從 JVM 層面,Java 線程狀態依然處於 RUNNABLE 狀態。JVM 並不關心操作系統線程實際狀態。從 JVM 看來等待 CPU 使用權(操作系統線程狀態為可運行狀態)與等待 I/O (操作系統線程狀態處於休眠狀態)沒有區別,都是在等待某種資源,所以都歸入 RUNNABLE 狀態。

其他 Java 線程狀態與操作線程狀態類似。

面試官:都說阻塞 I/O 模型將會使線程休眠,為什麼 Java 線程狀態卻是 RUNNABLE?
其他平台.png