面試官問:爲何 Java 線程沒有Running狀態?我懵了

Java虛擬機層面所暴露給咱們的狀態,與操做系統底層的線程狀態是兩個不一樣層面的事。具體而言,這裏說的 Java 線程狀態均來自於 Thread 類下的 State 這一內部枚舉類中所定義的狀態:java

圖片

什麼是 RUNNABLE?

直接看它的 Javadoc 中的說明:面試

一個在 JVM 中執行 的線程處於這一狀態中。(A thread executing in the Java virtual machine is in this state.)markdown

而傳統的進(線)程狀態通常劃分以下:網絡

圖片

注:這裏的進程指早期的單線程 進程,這裏所謂進程狀態實質就是線程狀態。架構

那麼 runnable 與圖中的 ready 與 running 區別在哪呢?併發

與傳統的ready狀態的區別

更具體點,javadoc 中是這樣說的:socket

處於 runnable 狀態下的線程正在 Java 虛擬機中執行,但它可能正在等待 來自於操做系統的其它資源,好比處理器。ide

A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.性能

顯然,runnable 狀態實質上是包括了 ready 狀態的。學習

甚至還可能有包括上圖中的 waiting 狀態的部分細分狀態,在後面咱們將會看到這一點。

與傳統的running狀態的區別

有人常以爲 Java 線程狀態中還少了個 running 狀態,這實際上是把兩個不一樣層面的狀態混淆了。對 Java 線程狀態而言,不存在所謂的running 狀態,它的 runnable 狀態包含了 running 狀態。

咱們可能會問,爲什麼 JVM 中沒有去區分這兩種狀態呢?

如今的時分 (time-sharing)多任務 (multi-task)操做系統架構一般都是用所謂的「時間分片 (time quantum or time slice)」方式進行搶佔式 (preemptive)輪轉調度(round-robin式)。

更復雜的可能還會加入優先級(priority)的機制。

這個時間分片一般是很小的,一個線程一次最多隻能在 cpu 上運行好比10-20ms 的時間(此時處於 running 狀態),也即大概只有0.01秒這一量級,時間片用後就要被切換下來放入調度隊列的末尾等待再次調度。(也即回到 ready 狀態)

注:若是期間進行了 I/O 的操做還會致使提早釋放時間分片,並進入等待隊列。

又或者是時間分片沒有用完就被搶佔,這時也是回到 ready 狀態。

這一切換的過程稱爲線程的上下文切換 (context switch),固然 cpu 不是簡單地把線程踢開就完了,還須要把被相應的執行狀態保存到內存中以便後續的恢復執行。

顯然,10-20ms 對人而言是很快的,

不計切換開銷(每次在1ms 之內),至關於1秒內有50-100次切換。事實上時間片常常沒用完,線程就由於各類緣由被中斷,實際發生的切換次數還會更多。

也這正是單核 *CPU 上實現所謂的「 併發*(concurrent)」的基本原理,但實際上是快速切換所帶來的假象,這有點相似一個手腳很是快的雜耍演員可讓好多個球同時在空中運轉那般。

時間分片也是可配置的,若是不追求在多個線程間很快的響應,也能夠把這個時間配置得大一點,以減小切換帶來的開銷。

若是是多核CPU,纔有可能實現真正意義上的併發,這種狀況一般也叫並行 (pararell),不過你可能也會看到這兩詞會被混着用,這裏就不去糾結它們的區別了。

一般,Java的線程狀態是服務於監控的,若是線程切換得是如此之快,那麼區分 ready 與 running 就沒什麼太大意義了。

當你看到監控上顯示是 running 時,對應的線程可能早就被切換下去了,甚至又再次地切換了上來,也許你只能看到 ready 與 running 兩個狀態在快速地閃爍。

固然,對於精確的性能評估而言,得到準確的 running 時間是有必要的。

現今主流的 JVM 實現都把 Java 線程一一映射到操做系統底層的線程上,把調度委託給了操做系統,咱們在虛擬機層面看到的狀態實質是對底層狀態的映射及包裝。JVM 自己沒有作什麼實質的調度,把底層的 ready 及 running 狀態映射上來也沒多大意義,所以,統一成爲runnable 狀態是不錯的選擇。

咱們將看到,Java 線程狀態的改變一般只與自身顯式引入的機制有關。

當I/O阻塞時

咱們知道傳統的I/O都是阻塞式(blocked)的,緣由是I/O操做比起cpu來實在是太慢了,可能差到好幾個數量級都說不定。若是讓 cpu 去等I/O 的操做,極可能時間片都用完了,I/O 操做還沒完成呢,無論怎樣,它會致使 cpu 的利用率極低。

因此,解決辦法就是:一旦線程中執行到 I/O 有關的代碼,相應線程立馬被切走,而後調度 ready 隊列中另外一個線程來運行。

這時執行了 I/O 的線程就再也不運行,即所謂的被阻塞了。它也不會被放到調度隊列中去,由於極可能再次調度到它時,I/O 可能仍沒有完成。

線程會被放到所謂的等待隊列中,處於上圖中的 waiting 狀態:

圖片

固然了,咱們所謂阻塞只是指這段時間 cpu 暫時不會理它了,但另外一個部件好比硬盤則在努力地爲它服務。cpu 與硬盤間是併發的。若是把線程視做爲一個 job,這一 job 由 cpu 與硬盤交替協做完成,當在 cpu 上是 waiting 時,在硬盤上卻處於 running,只是咱們在操做系統層面討論線程狀態時一般是圍繞着 cpu 這一中心去述說的。

而當 I/O 完成時,則用一種叫中斷 (interrupt)的機制來通知 cpu:

也即所謂的「中斷驅動 (interrupt-driven)」,現代操做系統基本都採用這一機制。

某種意義上,這也是控制反轉 (IoC)機制的一種體現,cpu不用反覆去詢問硬盤,這也是所謂的「好萊塢原則」—Don’t call us, we will call you.好萊塢的經紀人常常對演員們說:「別打電話給我,(有戲時)咱們會打電話給你。」

在這裏,硬盤與 cpu 的互動機制也是相似,硬盤對 cpu 說:」別老來問我 IO 作完了沒有,完了我天然會通知你的「

固然了,cpu 仍是要不斷地檢查中斷,就比如演員們也要時刻注意接聽電話,不過這總好過不斷主動去詢問,畢竟絕大多數的詢問都將是徒勞的。

cpu 會收到一個好比說來自硬盤的中斷信號,並進入中斷處理例程,手頭正在執行的線程所以被打斷,回到 ready 隊列。而先前因 I/O 而waiting 的線程隨着 I/O 的完成也再次回到 ready 隊列,這時 cpu 可能會選擇它來執行。

另外一方面,所謂的時間分片輪轉本質上也是由一個定時器定時中斷來驅動的,可使線程從 running 回到 ready 狀態:

圖片

好比設置一個10ms 的倒計時,時間一到就發一箇中斷,好像大限已到同樣,而後重置倒計時,如此循環。

與 cpu 正打得火熱的線程可能不情願聽到這一中斷信號,由於它意味着這一次與 cpu 纏綿的時間又要到頭了......奴爲出來難,何日君再來?

如今咱們再看一下 Java 中定義的線程狀態,嘿,它也有 BLOCKED(阻塞),也有 WAITING(等待),甚至它還更細,還有TIMED_WAITING:

圖片

如今問題來了,進行阻塞式 I/O 操做時,Java 的線程狀態到底是什麼?是 BLOCKED?仍是 WAITING?

可能你已經猜到,既然放到 RUNNABLE 這一主題下討論,其實狀態仍是 RUNNABLE。咱們也能夠經過一些測試來驗證這一點:

`@Test`
`public void testInBlockedIOState() throws InterruptedException {`
 `Scanner in = new Scanner(System.in);`
 `// 建立一個名爲「輸入輸出」的線程t`
 `Thread t = new Thread(new Runnable() {`
 `@Override`
 `public void run() {`
 `try {`
 `// 命令行中的阻塞讀`
 `String input = in.nextLine();`
 `System.out.println(input);`
 `} catch (Exception e) {`
 `e.printStackTrace();`
 `} finally {`
 `IOUtils.closeQuietly(in);`
 `}`
 `}`
 `}, "輸入輸出"); // 線程的名字`
 `// 啓動`
 `t.start();`
 `// 確保run已經獲得執行`
 `Thread.sleep(100);`
 `// 狀態爲RUNNABLE`
 `assertThat(t.getState()).isEqualTo(Thread.State.RUNNABLE);`
`}`

複製代碼

在最後的語句上加一斷點,監控上也反映了這一點:

圖片

網絡阻塞時同理,好比socket.accept,咱們說這是一個「阻塞式(blocked)」式方法,但線程狀態仍是 RUNNABLE。

`@Test`
`public void testBlockedSocketState() throws Exception {`
 `Thread serverThread = new Thread(new Runnable() {`
 `@Override`
 `public void run() {`
 `ServerSocket serverSocket = null;`
 `try {`
 `serverSocket = new ServerSocket(10086);`
 `while (true) {`
 `// 阻塞的accept方法`
 `Socket socket = serverSocket.accept();`
 `// TODO`
 `}`
 `} catch (IOException e) {`
 `e.printStackTrace();`
 `} finally {`
 `try {`
 `serverSocket.close();`
 `} catch (IOException e) {`
 `e.printStackTrace();`
 `}`
 `}`
 `}`
 `}, "socket線程"); // 線程的名字`
 `serverThread.start();`
 `// 確保run已經獲得執行`
 `Thread.sleep(500);`
 `// 狀態爲RUNNABLE`
 `assertThat(serverThread.getState()).isEqualTo(Thread.State.RUNNABLE);`
`}`

複製代碼

監控顯示:

圖片

固然,Java 很早就引入了所謂 nio(新的IO)包,至於用 nio 時線程狀態到底是怎樣的,這裏就再也不一一具體去分析了。

至少咱們看到了,進行傳統上的 IO 操做時,口語上咱們也會說「阻塞」,但這個「阻塞」與線程的 BLOCKED 狀態是兩碼事!

如何看待RUNNABLE狀態?

首先仍是前面說的,注意分清兩個層面:

圖片

虛擬機是騎在你操做系統上面的,身下的操做系統是做爲某種資源爲知足虛擬機的需求而存在的。

當進行阻塞式的 IO 操做時,或許底層的操做系統線程確實處在阻塞狀態,但咱們關心的是 JVM 的線程狀態。

JVM 並不關心底層的實現細節,什麼時間分片也好,什麼 IO 時就要切換也好,它並不關心。

前面說到,「處於 runnable 狀態下的線程正在* Java 虛擬機中執行,但它 可能正在等待*來自於操做系統的其它資源,好比處理器。」

JVM 把那些都視做資源,cpu 也好,硬盤,網卡也罷,有東西在爲線程服務,它就認爲線程在「執行」。

你用嘴,用手,仍是用什麼鳥東西來知足它的需求,它並不關心~

處於 IO 阻塞,只是說 cpu 不執行線程了,但網卡可能還在監聽呀,雖然可能暫時沒有收到數據:

就比如前臺或保安坐在他們的位置上,可能沒有接待什麼人,但你能說他們沒在工做嗎?

因此 JVM 認爲線程還在執行。而操做系統的線程狀態是圍繞着 cpu 這一核心去述說的,這與 JVM 的側重點是有所不一樣的。

前面咱們也強調了「Java 線程狀態的改變一般只與自身顯式引入的機制有關」,若是 JVM 中的線程狀態發生改變了,一般是自身機制引起的。

好比 synchronize 機制有可能讓線程進入BLOCKED 狀態,sleep,wait等方法則可能讓其進入 WATING 之類的狀態。

它與傳統的線程狀態的對應能夠以下來看:

圖片

RUNNABLE 狀態對應了傳統的 ready, running 以及部分的 waiting 狀態。

送你們如下java學習資料,回覆:面試題和實戰項目便可領取

面試題1.png

面試題2.png

實戰項目1.png

實戰項目2.png

相關文章
相關標籤/搜索