面試官:為什么Java線程沒有Running狀態(tài)?
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進!你不來,我和你的競爭對手一起精進!
編輯:業(yè)余草
推薦:https://www.xttblog.com/?p=5261
今天在正文開始之前,我們先來一個投票,感謝大家!
Java虛擬機層面所暴露給我們的狀態(tài),與操作系統(tǒng)底層的線程狀態(tài)是兩個不同層面的事。具體而言,這里說的 Java 線程狀態(tài)均來自于 Thread 類下的 State 這一內(nèi)部枚舉類中所定義的狀態(tài):

什么是 RUNNABLE?
直接看它的 Javadoc 中的說明:
?一個在 JVM 中「執(zhí)行」的線程處于這一狀態(tài)中。(A thread 「executing」 in the Java virtual machine is in this state.)
?
而傳統(tǒng)的進(線)程狀態(tài)一般劃分如下:

?注:這里的進程指早期的「單線程」進程,這里所謂進程狀態(tài)實質(zhì)就是線程狀態(tài)。
?
那么 runnable 與圖中的 ready 與 running 區(qū)別在哪呢?
與傳統(tǒng)的ready狀態(tài)的區(qū)別
更具體點,javadoc 中是這樣說的:
?處于 runnable 狀態(tài)下的線程正在 Java 虛擬機中執(zhí)行,但它「可能正在等待」來自于操作系統(tǒng)的其它資源,比如處理器。
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 狀態(tài)實質(zhì)上是包括了 ready 狀態(tài)的。
?甚至還可能有包括上圖中的 waiting 狀態(tài)的部分細(xì)分狀態(tài),在后面我們將會看到這一點。
?
與傳統(tǒng)的running狀態(tài)的區(qū)別
有人常覺得 Java 線程狀態(tài)中還少了個 running 狀態(tài),這其實是把兩個不同層面的狀態(tài)混淆了。對 Java 線程狀態(tài)而言,不存在所謂的running 狀態(tài),它的 runnable 狀態(tài)包含了 running 狀態(tài)。
我們可能會問,為何 JVM 中沒有去區(qū)分這兩種狀態(tài)呢?
現(xiàn)在的「時分」(time-sharing)「多任務(wù)」(multi-task)操作系統(tǒng)架構(gòu)通常都是用所謂的“「時間分片」(time quantum or time slice)”方式進行「搶占式」(preemptive)輪轉(zhuǎn)調(diào)度(round-robin式)。
?更復(fù)雜的可能還會加入優(yōu)先級(priority)的機制。
?
這個時間分片通常是很小的,一個線程一次最多只能在 cpu 上運行比如10-20ms 的時間(此時處于 running 狀態(tài)),也即大概只有0.01秒這一量級,時間片用后就要被切換下來放入調(diào)度隊列的末尾等待再次調(diào)度。(也即回到 ready 狀態(tài))
?注:如果期間進行了 I/O 的操作還會導(dǎo)致提前釋放時間分片,并進入等待隊列。
又或者是時間分片沒有用完就被搶占,這時也是回到 ready 狀態(tài)。
?
這一切換的過程稱為線程的「上下文切換」(context switch),當(dāng)然 cpu 不是簡單地把線程踢開就完了,還需要把被相應(yīng)的執(zhí)行狀態(tài)保存到內(nèi)存中以便后續(xù)的恢復(fù)執(zhí)行。
顯然,10-20ms 對人而言是很快的,
?不計切換開銷(每次在1ms 以內(nèi)),相當(dāng)于1秒內(nèi)有50-100次切換。事實上時間片經(jīng)常沒用完,線程就因為各種原因被中斷,實際發(fā)生的切換次數(shù)還會更多。
?
也這正是**單核 *CPU 上實現(xiàn)所謂的“**并發(fā)*(concurrent)”的基本原理,但其實是快速切換所帶來的假象,這有點類似一個手腳非??斓碾s耍演員可以讓好多個球同時在空中運轉(zhuǎn)那般。
?時間分片也是可配置的,如果不追求在多個線程間很快的響應(yīng),也可以把這個時間配置得大一點,以減少切換帶來的開銷。
如果是多核CPU,才有可能實現(xiàn)真正意義上的并發(fā),這種情況通常也叫「并行」(pararell),不過你可能也會看到這兩詞會被混著用,這里就不去糾結(jié)它們的區(qū)別了。
?
通常,Java的線程狀態(tài)是服務(wù)于監(jiān)控的,如果線程切換得是如此之快,那么區(qū)分 ready 與 running 就沒什么太大意義了。
?當(dāng)你看到監(jiān)控上顯示是 running 時,對應(yīng)的線程可能早就被切換下去了,甚至又再次地切換了上來,也許你只能看到 ready 與 running 兩個狀態(tài)在快速地閃爍。
當(dāng)然,對于精確的性能評估而言,獲得準(zhǔn)確的 running 時間是有必要的。
?
現(xiàn)今主流的 JVM 實現(xiàn)都把 Java 線程一一映射到操作系統(tǒng)底層的線程上,把調(diào)度委托給了操作系統(tǒng),我們在虛擬機層面看到的狀態(tài)實質(zhì)是對底層狀態(tài)的映射及包裝。JVM 本身沒有做什么實質(zhì)的調(diào)度,把底層的 ready 及 running 狀態(tài)映射上來也沒多大意義,因此,統(tǒng)一成為runnable 狀態(tài)是不錯的選擇。
?我們將看到,Java 線程狀態(tài)的改變通常只與自身顯式引入的機制有關(guān)。
?
當(dāng)I/O阻塞時
我們知道傳統(tǒng)的I/O都是阻塞式(blocked)的,原因是I/O操作比起cpu來實在是太慢了,可能差到好幾個數(shù)量級都說不定。如果讓 cpu 去等I/O 的操作,很可能時間片都用完了,I/O 操作還沒完成呢,不管怎樣,它會導(dǎo)致 cpu 的利用率極低。
所以,解決辦法就是:一旦線程中執(zhí)行到 I/O 有關(guān)的代碼,相應(yīng)線程立馬被切走,然后調(diào)度 ready 隊列中另一個線程來運行。
這時執(zhí)行了 I/O 的線程就不再運行,即所謂的被阻塞了。它也不會被放到調(diào)度隊列中去,因為很可能再次調(diào)度到它時,I/O 可能仍沒有完成。
線程會被放到所謂的等待隊列中,處于上圖中的 waiting 狀態(tài):

當(dāng)然了,我們所謂阻塞只是指這段時間 cpu 暫時不會理它了,但另一個部件比如硬盤則在努力地為它服務(wù)。cpu 與硬盤間是并發(fā)的。如果把線程視作為一個 job,這一 job 由 cpu 與硬盤交替協(xié)作完成,當(dāng)在 cpu 上是 waiting 時,在硬盤上卻處于 running,只是我們在操作系統(tǒng)層面討論線程狀態(tài)時通常是圍繞著 cpu 這一中心去述說的。
而當(dāng) I/O 完成時,則用一種叫「中斷」(interrupt)的機制來通知 cpu:
也即所謂的“「中斷驅(qū)動」(interrupt-driven)”,現(xiàn)代操作系統(tǒng)基本都采用這一機制。
某種意義上,這也是「控制反轉(zhuǎn)」(IoC)機制的一種體現(xiàn),cpu不用反復(fù)去詢問硬盤,這也是所謂的“好萊塢原則”—Don’t call us, we will call you.好萊塢的經(jīng)紀(jì)人經(jīng)常對演員們說:“別打電話給我,(有戲時)我們會打電話給你?!?/p>
在這里,硬盤與 cpu 的互動機制也是類似,硬盤對 cpu 說:”別老來問我 IO 做完了沒有,完了我自然會通知你的“
當(dāng)然了,cpu 還是要不斷地檢查中斷,就好比演員們也要時刻注意接聽電話,不過這總好過不斷主動去詢問,畢竟絕大多數(shù)的詢問都將是徒勞的。
cpu 會收到一個比如說來自硬盤的中斷信號,并進入中斷處理例程,手頭正在執(zhí)行的線程因此被打斷,回到 ready 隊列。而先前因 I/O 而waiting 的線程隨著 I/O 的完成也再次回到 ready 隊列,這時 cpu 可能會選擇它來執(zhí)行。
另一方面,所謂的時間分片輪轉(zhuǎn)本質(zhì)上也是由一個定時器定時中斷來驅(qū)動的,可以使線程從 running 回到 ready 狀態(tài):

比如設(shè)置一個10ms 的倒計時,時間一到就發(fā)一個中斷,好像大限已到一樣,然后重置倒計時,如此循環(huán)。
?與 cpu 正打得火熱的線程可能不情愿聽到這一中斷信號,因為它意味著這一次與 cpu 纏綿的時間又要到頭了......奴為出來難,何日君再來?
?
現(xiàn)在我們再看一下 Java 中定義的線程狀態(tài),嘿,它也有 BLOCKED(阻塞),也有 WAITING(等待),甚至它還更細(xì),還有TIMED_WAITING:

現(xiàn)在問題來了,進行阻塞式 I/O 操作時,Java 的線程狀態(tài)究竟是什么?是 BLOCKED?還是 WAITING?
可能你已經(jīng)猜到,既然放到 RUNNABLE 這一主題下討論,其實狀態(tài)還是 RUNNABLE。我們也可以通過一些測試來驗證這一點:
@Test
public void testInBlockedIOState() throws InterruptedException {
Scanner in = new Scanner(System.in);
// 創(chuàng)建一個名為“輸入輸出”的線程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已經(jīng)得到執(zhí)行
Thread.sleep(100);
// 狀態(tài)為RUNNABLE
assertThat(t.getState()).isEqualTo(Thread.State.RUNNABLE);
}
在最后的語句上加一斷點,監(jiān)控上也反映了這一點:

網(wǎng)絡(luò)阻塞時同理,比如socket.accept,我們說這是一個“阻塞式(blocked)”式方法,但線程狀態(tài)還是 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已經(jīng)得到執(zhí)行
Thread.sleep(500);
// 狀態(tài)為RUNNABLE
assertThat(serverThread.getState()).isEqualTo(Thread.State.RUNNABLE);
}
監(jiān)控顯示:

當(dāng)然,Java 很早就引入了所謂 nio(新的IO)包,至于用 nio 時線程狀態(tài)究竟是怎樣的,這里就不再一一具體去分析了。
?至少我們看到了,進行傳統(tǒng)上的 IO 操作時,口語上我們也會說“阻塞”,但這個“阻塞”與線程的 BLOCKED 狀態(tài)是兩碼事!
?
如何看待RUNNABLE狀態(tài)?
首先還是前面說的,注意分清兩個層面:

虛擬機是騎在你操作系統(tǒng)上面的,身下的操作系統(tǒng)是作為某種資源為滿足虛擬機的需求而存在的:

當(dāng)進行阻塞式的 IO 操作時,或許底層的操作系統(tǒng)線程確實處在阻塞狀態(tài),但我們關(guān)心的是 JVM 的線程狀態(tài)。
?JVM 并不關(guān)心底層的實現(xiàn)細(xì)節(jié),什么時間分片也好,什么 IO 時就要切換也好,它并不關(guān)心。
?
前面說到,“處于 runnable 狀態(tài)下的線程正在* Java 虛擬機「中執(zhí)行,但它」可能正在等待*來自于操作系統(tǒng)的其它資源,比如處理器?!?/p>
JVM 把那些都視作資源,cpu 也好,硬盤,網(wǎng)卡也罷,有東西在為線程服務(wù),它就認(rèn)為線程在“執(zhí)行”。
?你用嘴,用手,還是用什么鳥東西來滿足它的需求,它并不關(guān)心~
?
處于 IO 阻塞,只是說 cpu 不執(zhí)行線程了,但網(wǎng)卡可能還在監(jiān)聽呀,雖然可能暫時沒有收到數(shù)據(jù):
?就好比前臺或保安坐在他們的位置上,可能沒有接待什么人,但你能說他們沒在工作嗎?
?
所以 JVM 認(rèn)為線程還在執(zhí)行。而操作系統(tǒng)的線程狀態(tài)是圍繞著 cpu 這一核心去述說的,這與 JVM 的側(cè)重點是有所不同的。
前面我們也強調(diào)了“Java 線程狀態(tài)的改變通常只與自身顯式引入的機制有關(guān)”,如果 JVM 中的線程狀態(tài)發(fā)生改變了,通常是自身機制引發(fā)的。
?比如 synchronize 機制有可能讓線程進入BLOCKED 狀態(tài),sleep,wait等方法則可能讓其進入 WATING 之類的狀態(tài)。
?
它與傳統(tǒng)的線程狀態(tài)的對應(yīng)可以如下來看:

RUNNABLE 狀態(tài)對應(yīng)了傳統(tǒng)的 ready, running 以及部分的 waiting 狀態(tài)。
