Java 線程的狀態及轉換
閃客:小宇你怎麼了,我看你臉色很不好呀。
小宇:今天去面試了,面試官問我 Java 線程的狀態及其轉化。
閃客:哦哦,很常見的面試題呀,不是有一張狀態流轉圖嘛。
小宇:我知道,可是我每次面試的時候,腦子裏記過的流轉圖就變成這樣了。
閃客:哈哈哈。
小宇:你還笑,氣死我了,你能不能給我講講這些亂七八糟的狀態呀。
閃客:沒問題,還是老規矩,你先把所有狀態都忘掉,聽我從頭道來!
小宇:好滴。
線程狀態的實質
===============
首先你得明白,當我們說一個線程的狀態時,說的是什麼?
沒錯,就是一個變量的值而已。
哪個變量?
Thread 類中的一個變量,叫
private volatile int threadStatus = 0;
這個值是個整數,不方便理解,可以通過映射關係(VM.toThreadState),轉換成一個枚舉類。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
所以,我們就盯着 threadStatus 這個值的變化就好了。
就是這麼簡單。
NEW
===========
現在我們還沒有任何 Thread 類的對象呢,也就不存在線程狀態一說。
一切的起點,要從把一個 Thread 類的對象創建出來,開始說起。
Thread t = new Thread();
當然,你後面可以接很多參數。
Thread t = new Thread(r, "name1");
你也可以 new 一個繼承了 Thread 類的子類。
Thread t = new MyThread();
你說線程池怎麼不 new 就可以有線程了呢?人家內部也是 new 出來的。
public class Executors {
static class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread(...);
...
return t;
}
}
}
總是,一切的開始,都要調用 Thread 類的構造方法。
而這個構造方法,最終都會調用 Thread 類的 init() 方法。
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
...
this.grout = g;
this.name = name;
...
tid = nextThreadID();
}
這個 init 方法,僅僅是給該 Thread 類的對象中的屬性,附上值,除此之外啥也沒幹。
它沒有給 theadStatus 再次賦值,所以它的值仍然是其默認值。
而這個值對應的狀態,就是 STATE.NEW,非要翻譯成中文,就叫初始態吧。
因此說了這麼多,其實就分析出了,新建一個 Thread 類的對象,就是創建了一個新的線程,此時這個線程的狀態,是 NEW(初始態)。
之後的分析,將弱化 threadStatus 這個整數值了,就直接說改變了其線程狀態,大家知道其實就只是改變了 threadStatus 的值而已。
RUNNABLE
================
你說,剛剛處於 NEW 狀態的線程,對應操作系統裏的什麼狀態呢?
一看你就沒仔細看我上面的分析。
Thread t = new Thread();
只是做了些表面功夫,在 Java 語言層面將自己的一個對象中的屬性附上值罷了,根本沒碰到操作系統級別的東西呢。
所以這個 NEW 狀態,不論往深了說還是往淺了說,還真就只是個無聊的枚舉值而已。
下面,精彩的故事纔剛剛開始。
躺在堆內存中無所事事的 Thread 對象,在調用了 start() 方法後,才顯現生機。
t.start();
這個方法一調用,那可不得了,最終會調用到一個討厭的 native 方法裏。
private native void start0();
看來改變狀態就並不是一句 threadStatus = xxx 這麼簡單了,而是有本地方法對其進行了修改。
九曲十八彎跟進 jvm 源碼之後,調用到了這個方法。
hotspot/src/os/linux/vm/os_linux.cpppthread_create(...);
大名鼎鼎的 unix 創建線程的方法,pthread_create。
此時,在操作系統內核中,纔有了一個真正的線程,被創建出來。
而 linux 操作系統,是沒有所謂的剛創建但沒啓動的線程這種說法的,創建即刻開始運行。
雖然無法從源碼發現線程狀態的變化,但通過 debug 的方式,我們看到調用了 Thread.start() 方法後,線程的狀態變成了 RUNNABLE,運行態。
那我們的狀態圖又豐富了起來。
通過這部分,我們知道如下幾點:
-
在 Java 調用 start() 後,操作系統中才真正出現了一個線程,並且立刻運行。
-
Java 中的線程,和操作系統內核中的線程,是一對一的關係。
-
調用 start 後,線程狀態變爲 RUNNABLE,這是由 native 方法裏的某部分代碼造成的。
RUNNING 和 READY
=======================
CPU 一個核心,同一時刻,只能運行一個線程。
具體執行哪個線程,要看操作系統 的調度機制。
所以,上面的 RUNNABLE 狀態,準確說是,得到了可以隨時準備運行的機會的狀態。
而處於這個狀態中的線程,也分爲了正在 CPU 中運行的線程,和一堆處於就緒中等待 CPU 分配時間片來運行的線程。
處於就緒中的線程,會存儲在一個就緒隊列中,等待着被操作系統的調度機制選到,進入 CPU 中運行。
當然,要注意,這裏的 RUNNING 和 READY 狀態,是我們自己爲了方便描述而造出來的。
無論是 Java 語言,還是操作系統,都不區分這兩種狀態,在 Java 中統統叫 RUNNABLE。
TERMINATED
==================
當一個線程執行完畢(或者調用已經不建議的 stop 方法),線程的狀態就變爲 TERMINATED。
此時這個線程已經無法死灰復燃了,如果你此時再強行執行 start 方法,將會報出錯誤。
java.lang.IllegalThreadStateException
很簡單,因爲 start 方法的第一行就是這麼直戳了當地寫的。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
...
}
誒,那如果此時強行把 threadStatus 改成 0,會怎麼樣呢?你可以試試喲。
BLOCKED
===============
上面把最常見,最簡單的線程生命週期講完了。
初始 -- 運行 -- 終止
沒有發生任何的障礙。
接下來,就稍稍複雜一點了,我們讓線程碰到些障礙。
首先創建一個對象 lock。
public static final Object lock = new Object();
一個線程,執行一個 sychronized 塊,鎖對象是 lock,且一直持有這把鎖不放。
new Thread(() -> {
synchronized (lock) {
while(true) {}
}
}).start();
另一個線程,也同樣執行一個鎖對象爲 lock 的 sychronized 塊。
new Thread(() -> {
synchronized (lock) {
...
}
}).start();
那麼,在進入 synchronized 塊時,因爲無法拿到鎖,會使線程狀態變爲 BLOCKED。
同樣,對於 synchronized 方法,也是如此。
當該線程獲取到了鎖後,便可以進入 synchronized 塊,此時線程狀態變爲 RUNNABLE。
因此我們得出如下轉換關係。
當然,這只是線程狀態的改變,線程還發生了一些實質性的變化。
我們不考慮虛擬機對 synchronized 的極致優化。
當進入 synchronized 塊或方法,獲取不到鎖時,線程會進入一個該鎖對象的同步隊列。
當持有鎖的這個線程,釋放了鎖之後,會喚醒該鎖對象同步隊列中的所有線程,這些線程會繼續嘗試搶鎖。如此往復。
比如,有一個鎖對象 A,線程 1 此時持有這把鎖。線程 2、3、4 分別嘗試搶這把鎖失敗。
線程 1 釋放鎖,線程 2、3、4 重新變爲 RUNNABLE,繼續搶鎖,假如此時線程 3 搶到了鎖。
如此往復。
WAITING
===============
這部分是最複雜的,同時也是面試中考點最多的,將分成三部分講解。聽我說完後你會發現,這三部分有很多相同但地方,不再是孤立的知識點。
wait/notify
我們在剛剛的 synchronized 塊中加點東西。
new Thread(() -> {
synchronized (lock) {
...
lock.wait();
...
}
}).start();
當這個 lock.wait() 方法一調用,會發生三件事。
-
釋放鎖對象 lock(隱含着必須先獲取到這個鎖纔行)
-
線程狀態變成 WAITING
-
線程進入 lock 對象的等待隊列
什麼時候這個線程被喚醒,從等待隊列中移出,並從 WAITING 狀態返回 RUNNABLE 狀態呢?
必須由另一個線程,調用同一個對象的 notify/notifyAll 方法。
new Thread(() -> {
synchronized (lock) {
...
lock.notify();
...
}
}).start();
只不過 notify 是隻喚醒一個線程,而 notifyAll 是喚醒所有等待隊列中的線程。
但需要注意,被喚醒後的線程,從等待隊列移出,狀態變爲 RUNNABLE,但仍然需要搶鎖,搶鎖成功了,纔可以從 wait 方法返回,繼續執行。
如果失敗了,就和上一部分的 BLOCKED 流程一樣了。
所以我們的整個流程圖,現在變成了這個樣子。
join
主線程這樣寫。
public static void main(String[] args) {
thread t = new Thread(...);
t.start();
t.join();
...
}
當執行到 t.join() 的時候,主線程會變成 WAITING 狀態,直到線程 t 執行完畢,主線程纔會變回 RUNNABLE 狀態,繼續往下執行。
看起來,就像是主線程執行過程中,另一個線程插隊加入(join),而且要等到其結束後主線程才繼續。
因此我們的狀態圖,又多了兩項。
那 join 又是怎麼神奇地實現這一切呢?也是像 wait 一樣放到等待隊列麼?
打開 Thread.join() 的源碼,你會發現它非常簡單。
// Thread.java
// 無參的 join 有用的信息就這些,省略了額外分支
public synchronized void join() {
while (isAlive()) {
wait();
}
}
也就是說,他的本質仍然是執行了 wait() 方法,而鎖對象就是 Thread t 對象本身。
那從 RUNNABLE 到 WAITING,就和執行了 wait() 方法完全一樣了。
那從 WAITING 回到 RUNNABLE 是怎麼實現的呢?
主線程調用了 wait ,需要另一個線程 notify 纔行,難道需要這個子線程 t 在結束之前,調用一下 t.notifyAll() 麼?
答案是否定的,那就只有一種可能,線程 t 結束後,由 jvm 自動調用 t.notifyAll(),不用我們程序顯示寫出。
沒錯,就是這樣。
怎麼證明這一點呢?道聽途說可不行,老子今天非要扒開 jvm 的外套。
果然,找到了如下代碼。
我們看到,虛擬機在一個線程的方法執行完畢後,執行了個 ensure_join 方法,看名字就知道是專門爲 join 而設計的。
而繼續跟進會發現一段關鍵代碼,lock.notify_all,這便是一個線程結束後,會自動調用自己的 notifyAll 方法的證明。
所以,其實 join 就是 wait,線程結束就是 notifyAll。現在,是不是更清晰了。
park/unpark
有了上面 wait 和 notify 的機制,下面就好理解了。
一個線程調用如下方法。
LockSupport.park()
該線程狀態會從 RUNNABLE 變成 WAITING、
另一個線程調用
LockSupport.unpark(Thread 剛剛的線程)
剛剛的線程會從 WAITING 回到 RUNNABLE
但從線程狀態流轉來看,與 wait 和 notify 相同。
從實現機制上看,他們甚至更爲簡單。
-
park 和 unpark 無需事先獲取鎖,或者說跟鎖壓根無關。
-
沒有什麼等待隊列一說,unpark 會精準喚醒某一個確定的線程。
-
park 和 unpark 沒有順序要求,可以先調用 unpark
關於第三點,就涉及到 park 的原理了,這裏我只簡單說明。
線程有一個計數器,初始值爲 0
調用 park 就是
如果這個值爲 0,就將線程掛起,狀態改爲 WAITING。如果這個值爲 1,則將這個值改爲 0,其餘的什麼都不做。
調用 unpark 就是
將這個值改爲 1
然後我用三個例子,你就基本明白了。
// 例子1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以運行到這");
// 例子2
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以運行到這");
// 例子3
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
LockSupport.park(); // WAITING
System.out.println("不可以運行到這");
park 的使用非常簡單,同時也是 JDK 中鎖實現的底層。它的 JVM 及操作系統層面的原理很複雜,改天可以專門找一節來講解。
現在我們的狀態圖,又可以更新了。
TIMED_WAITING
這部分就再簡單不過了,將上面導致線程變成 WAITING 狀態的那些方法,都增加一個超時參數,就變成了將線程變成 TIMED_WAITING 狀態的方法了,我們直接更新流程圖。
這些方法的唯一區別就是,從 TIMED_WAITING 返回 RUNNABLE,不但可以通過之前的方式,還可以通過到了超時時間,返回 RUNNABLE 狀態。
就這樣。
還有,大家看。
wait 需要先獲取鎖,再釋放鎖,然後等待被 notify。
join 就是 wait 的封裝。
park 需要等待 unpark 來喚醒,或者提前被 unpark 發放了喚醒許可。
那有沒有一個方法,僅僅讓線程掛起,只能通過等待超時時間到了再被喚醒呢。
這個方法就是
Thread.sleep(long)
我們把它補充在圖裏,這一部分就全了。
再把它加到全局圖中。
後記
====================
Java 線程的狀態,有六種
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
而經典的線程五態模型,有五種狀態
創建
就緒
執行
阻塞
終止
不同實現者,可能有合併和拆分。
比如 Java 將五態模型中的就緒和執行,都統一成 RUNNABLE,將阻塞(即不可能得到 CPU 運行機會的狀態)細分爲了 BLOCKED、WAITING、TIMED_WAITING,這裏我們不去評價好壞。
也就是說,BLOCKED、WAITING、TIMED_WAITING 這幾個狀態,線程都不可能得到 CPU 的運行權,你叫它掛起、阻塞、睡眠、等待,都可以,很多文章,你也會看到這幾個詞沒那麼較真地來回用。
再說兩個你可能困惑的問題。
調用 jdk 的 Lock 接口中的 lock,如果獲取不到鎖,線程將掛起,此時線程的狀態是什麼呢?
有多少同學覺得應該和 synchronized 獲取不到鎖的效果一樣,是變成 BLOCKED 狀態?
不過如果你仔細看我上面的文章,有一句話提到了,jdk 中鎖的實現,是基於 AQS 的,而 AQS 的底層,是用 park 和 unpark 來掛起和喚醒線程,所以應該是變爲 WAITING 或 TIMED_WAITING 狀態。
調用阻塞 IO 方法,線程變成什麼狀態?
比如 socket 編程時,調用如 accept(),read() 這種阻塞方法時,線程處於什麼狀態呢?
答案是處於 RUNNABLE 狀態,但實際上這個線程是得不到運行權的,因爲在操作系統層面處於阻塞態,需要等到 IO 就緒,才能變爲就緒態。
但是在 Java 層面,JVM 認爲等待 IO 與等待 CPU 執行權,都是一樣的,人家就是這麼認爲的,這裏我仍然不討論其好壞,你覺得這麼認爲不爽,可以自己設計一門語言,那你想怎麼認爲,別人也拿你沒辦法。
比如要我設計語言,我就認爲可被 CPU 調度執行的線程,處於死亡態。這樣我的這門語言一定會有個經典面試題,爲什麼閃客把可運行的線程定義爲死亡態呢?
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/kjeWEXJfDz5pM3HLhYs9Xw