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;
}
}
public class Executors {
static class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread();
return t;
}
}
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.cpp
pthread_create();
大名鼎鼎的 unix 創建線程的方法,pthread_create。
此時,在操作系統內核中,才有了一個真正的線程,被創建出來。
而 linux 操作系統,是沒有所謂的剛創建但沒啟動的線程這種說法的,創建即刻開始運行。
雖然無法從源碼發現線程狀態的變化,但通過 debug 的方式,我們看到調用了 Thread.start () 方法后,線程的狀態變成了 RUNNABLE,運行態。
那我們的狀態圖又豐富了起來。
通過這部分,我們知道如下幾點:
1. 在 Java 調用 start () 后,操作系統中才真正出現了一個線程,并且立刻運行。
2. Java 中的線程,和操作系統內核中的線程,是一對一的關系。
3. 調用 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 () 方法一調用,會發生三件事。
1. 釋放鎖對象 lock(隱含著必須先獲取到這個鎖才行)
2. 線程狀態變成 WAITING
3. 線程進入 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 的外套。
果然,找到了如下代碼。
hotspot/src/share/vm/runtime/thread.cpp
void JavaThread::exit(...) {
...
ensure_join(this);
...
}
static void ensure_join(JavaThread* thread) {
...
lock.notify_all(thread);
...
}
我們看到,虛擬機在一個線程的方法執行完畢后,執行了個 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 相同。
從實現機制上看,他們甚至更為簡單。
1. park 和 unpark 無需事先獲取鎖,或者說跟鎖壓根無關。
2. 沒有什么等待隊列一說,unpark 會精準喚醒某一個確定的線程。
3. 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 調度執行的線程,處于死亡態。這樣我的這門語言一定會有個經典面試題,為什么閃客把可運行的線程定義為死亡態呢?
OK,今天的文章就到這里。
本篇文章寫得有點投入,寫到這發現把開頭都小宇都給忘了。