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