1. 程式人生 > 實用技巧 >面試官問:為什麼 Java 執行緒沒有 Running 狀態?我懵了

面試官問:為什麼 Java 執行緒沒有 Running 狀態?我懵了

  • 什麼是 RUNNABLE?

  • 與傳統的ready狀態的區別

  • 與傳統的running狀態的區別

  • 當I/O阻塞時

  • 如何看待RUNNABLE狀態?

Java虛擬機器層面所暴露給我們的狀態,與作業系統底層的執行緒狀態是兩個不同層面的事。具體而言,這裡說的 Java 執行緒狀態均來自於 Thread 類下的 State 這一內部列舉類中所定義的狀態:

什麼是 RUNNABLE?

直接看它的 Javadoc 中的說明:

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

而傳統的進(線)程狀態一般劃分如下:

注:這裡的程序指早期的單執行緒程序,這裡所謂程序狀態實質就是執行緒狀態。

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

與傳統的ready狀態的區別

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

處於 runnable 狀態下的執行緒正在 Java 虛擬機器中執行,但它可能正在等待來自於作業系統的其它資源,比如處理器。
A thread in the runnable state is executing in the Java virtual machine but it may be waiting forother 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 狀態。

來源:http://rrd.me/ekN5T

歡迎關注我的微信公眾號「碼農突圍」,分享Python、Java、大資料、機器學習、人工智慧等技術,關注碼農技術提升•職場突圍•思維躍遷,20萬+碼農成長充電第一站,陪有夢想的你一起成長