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