天天看點

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

>>号外:關注“Java精選”公衆号,回複“2021面試題”,領取免費資料!“Java精選面試題”小程式,3000+ 道面試題線上刷,最新、最全 Java 面試題!

Java虛拟機層面所暴露給我們的狀态,與作業系統底層的線程狀态是兩個不同層面的事。

具體而言,這裡說的 Java 線程狀态均來自于 Thread 類下的 State 這一内部枚舉類中所定義的狀态:

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

什麼是 RUNNABLE?

直接看它的 Javadoc 中的說明:

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

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

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?
注:這裡的程序指早期的單線程程序,這裡所謂程序狀态實質就是線程狀态。

那麼 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 狀态:

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

當然了,我們所謂阻塞隻是指這段時間 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 狀态:

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

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

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

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

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

現在問題來了,進行阻塞式 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);
}

           

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

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待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 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

當然,Java 很早就引入了所謂 nio(新的IO)包,至于用 nio 時線程狀态究竟是怎樣的,這裡就不再一一具體去分析了。

至少我們看到了,進行傳統上的 IO 操作時,口語上我們也會說“阻塞”,但這個“阻塞”與線程的 BLOCKED 狀态是兩碼事!

如何看待RUNNABLE狀态?

首先還是前面說的,注意厘清兩個層面:

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

虛拟機是騎在你作業系統上面的,身下的作業系統是作為某種資源為滿足虛拟機的需求而存在的:

當進行阻塞式的 IO 操作時,或許底層的作業系統線程确實處在阻塞狀态,但我們關心的是 JVM 的線程狀态。

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

前面說到,“處于 runnable 狀态下的線程正在* Java 虛拟機中執行,但它可能正在等待*來自于作業系統的其它資源,比如處理器。”

JVM 把那些都視作資源,cpu 也好,硬碟,網卡也罷,有東西在為線程服務,它就認為線程在“執行”。

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

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

是以 JVM 認為線程還在執行。而作業系統的線程狀态是圍繞着 cpu 這一核心去述說的,這與 JVM 的側重點是有所不同的。

前面我們也強調了“Java 線程狀态的改變通常隻與自身顯式引入的機制有關”,如果 JVM 中的線程狀态發生改變了,通常是自身機制引發的。

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

它與傳統的線程狀态的對應可以如下來看:

面試官問:為什麼 Java 線程沒有 Running 狀态?一下被問懵!什麼是 RUNNABLE?與傳統的ready狀态的差別與傳統的running狀态的差別當I/O阻塞時如何看待RUNNABLE狀态?

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

作者:國棟

my.oschina.net/goldenshaw/blog/705397

往期精選  點選标題可跳轉

SpringBoot + Mybatis + Druid + PageHelper 實作多資料源并分頁(附源碼)

Intellij IDEA 中的各種調試代碼技巧,輕松定位 Bug 問題(涵蓋超全面)

MyBatis 真坑!Integer 類型指派 0 ,當 != '' 時無法通過判斷執行 SQL 語句

面試官問:Spring Boot 中實作通用 Auth 認證,有哪幾種方式?

Spring 中 IService 有多個實作類,它是如何知道該注入哪個 ServiceImpl 類?

突然慌了!面試官問:線程池中多餘的線程是如何回收的?

MySQL 資料庫中百萬級資料量,大神是如何分頁查詢?

資料庫中 SQL 語句使用索引,還是很慢?可能是這幾點原因

Spring Boot 架構中實作跨域通路的五種解決方案,你懂了嗎?

面試官問:導緻 Spring 事務失效的場景有哪些,如何解決失效問題?

推薦 IntelliJ IDEA 特别“養眼”的主題插件,這幾款超贊,總有一款适合你!

點個贊,就知道你“在看”!