歡迎來到《併發王者課》,本文是該系列文章中的第8篇。
在本篇文章中,我將從多執行緒的本質出發,為你介紹執行緒相關的狀態和它們的變遷方式,並幫助你掌握這塊知識點。
一、多執行緒的本質是分工協作
如果你是王者的玩家,那麼你一定知道王者中的眾多英雄分為主要分為幾類,比如法師、戰士、坦克、輔助等等。一些玩家對這些分類可能並不瞭解,甚至會覺得,幹嘛要搞得這麼複雜,幹不完了嘛。這...當然不可以!
抱此想法的如果不是青銅玩家,想必就是戰場上的那些個人英雄主義玩家,在他們眼裡沒有團隊。然而,只有王者知道,比賽勝利的關鍵,在於團隊的分工協作。各自為戰必將一團亂麻、潰不成軍,正所謂單絲不成線,獨木難成林。
分工協作無處不在,峽谷中需要分工協作,現實中我們的工作更是社會化分工的結果,因為社會的本質就是分工協作。
而我要告訴你的是,在併發程式設計裡,多執行緒的本質也是分工協作,每個執行緒恰似一個英雄,有著自己的職責、狀態和技能(動作方法)。所謂執行緒的狀態、方法實現不過都是為了完成執行緒間的分工協作。換句話說,執行緒狀態的存在不是目的,而是實現分工協作的方式。所以,理解執行緒的執行緒狀態和驅動方法,首先要理解它們為什麼而存在。
二、從協作認知執行緒的狀態
執行緒的狀態是執行緒在協作過程中的瞬時特徵。根據協作的需要,執行緒總共有六種狀態,分別是NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED和TERMINATED等。比如,我們建立一個英雄哪吒的執行緒neZhaPlayer
:
Thread neZhaPlayer = new Thread(new NeZhaRunnable());
那麼,執行緒建立之後,接下來它將在下圖所示的六種狀態中變遷。剛建立的執行緒處於NEW的狀態,而如果我們呼叫neZhaPlayer.start()
,那它將會進入RUNNABLE狀態。
六種不同狀態的含義是這樣的:
- NEW:執行緒新建但尚未啟動時所處的狀態,比如上面的
neZhaPlayer
; - RUNNABLE:在 Java 虛擬機器中執行的執行緒所處狀態。需要注意的是,雖然執行緒當前正在被執行,但可能正在等待其他執行緒釋放資源;
- WAITING:無限期等待另一個執行緒執行特定操作來解除自己的等待狀態;
- TIMED_WAITING:限時等待另一個執行緒執行或自我解除等待狀態;
- BLOCKED:被阻塞等待其他執行緒釋放Monitor Lock;
- TERMINATED:執行緒執行結束。
在任意特定時刻,一個執行緒都只能處於上述六種狀態中的一種。需要你注意的是RUNNABLE這個狀態,它有些特殊。確切地說,它包含READY和RUNNING兩個細分狀態,下一章節的圖示中有明確標示。
另外,前面我們已經介紹過Thread類,對於執行緒各狀態的表述,你可以直接閱讀JDK中的Thread.State
列舉,並可以通過Thread.getState()
檢視當前執行緒的瞬時狀態。
三、從執行緒狀態變遷看背後的方法驅動
和人類的交流類似,在多執行緒的協作時,它們也需要交流。所以,執行緒狀態的變遷需就要不同的方法來實現交流,比如剛建立的執行緒需要通過呼叫start()
將執行緒狀態由NEW變遷為RUNNABLE。
下圖所展示的正是執行緒間的狀態變遷以及相關的驅動方法,你可以先大概瀏覽一遍,隨後再結合下文的各關鍵方法的表述深入理解。
需要注意的是,本文不會詳細介紹執行緒狀態相關的所有方法,這既不現實也毫無必要。上面這幅寶藏圖示是理解本文所述知識的核心,下面所介紹的幾個主要方法也並非為了你記憶,而是為了讓你更好理解上面這幅圖。
在你理解了這幅寶圖之後,你便可以完全自行去了解其他更多的方法。
1. start:對戰開始,敵軍還有5秒到達戰場
public class NeZhaRunnable implements Runnable {
public void run() {
System.out.println("我是哪吒,我去上路");
}
}
Thread neZhaPlayer = new Thread(new NeZhaRunnable());
neZhaPlayer.start();
start()
方法主要將完成執行緒狀態從NEW到RUNNABLE的變遷,這裡有兩個點:
- 建立新的執行緒;
- 由新的執行緒執行其中的
run()
方法。
需要注意的是,你不可以重複呼叫start()
方法,否則會丟擲IllegalThreadStateException
異常。
2. wait和notify:我在等你,好了請告訴我
哪吒每次在使用完大招後,都需要經歷幾十秒的冷卻時間才可以再次使用,接下來我們通過程式碼片段來模擬這個過程。
我們先定義一個Player
類,這個類中包含了fight()
和refreshSkills()
兩個方法,分別用於進攻和技能重新整理,程式碼片段如下。
public class Player {
public void fight() {
System.out.println("大招未就緒,冷卻中...");
synchronized (this) {
try {
this.wait();
System.out.println("大招已就緒,發起進攻!");
} catch (InterruptedException e) {
System.out.println("大招冷卻被中斷!");
}
}
}
public void refreshSkills() {
System.out.println("技能重新整理中...");
synchronized (this) {
this.notifyAll();
System.out.println("技能已重新整理!");
}
}
}
隨後,我們寫一段main()
方法使用剛才建立的Player
。注意,這裡我們建立了兩個執行緒分別呼叫Player
中的不同方法。
public static void main(String[] args) throws InterruptedException {
final Player neZha = new Player();
Thread neZhaFightThread = new Thread() {
public void run() {
neZha.fight();
}
};
Thread skillRefreshThread = new Thread() {
public void run() {
neZha.refreshSkills();
}
};
neZhaFightThread.start();
skillRefreshThread.start();
}
程式碼執行結果如下:
大招未就緒,冷卻中...
技能重新整理中...
技能已重新整理!
大招已就緒,發起進攻!
Process finished with exit code 0
從執行的結果看,符合預期。相信你已經看到了,在上面的程式碼中我們使用了wait()
和notify()
兩個函式。這兩個執行緒是如何協作的呢?往下看。
首先,neZhaAttachThread呼叫了neZha.fight()
這個方法。可是,當哪吒想發起進攻的時候,竟然大招還沒有冷卻!於是,這個執行緒不得不通過wait()
方法進入等待佇列。
緊接著,skillRefreshThread呼叫了neZha.refreshSkills()
這個方法。並且,在執行結束後又呼叫了notify()
方法。有趣的事情發生了,前面處於等待佇列中的neZhaAttachThread竟然又“復活”了,並且大喊了一聲:大招已經就緒,發起進攻!
這是怎麼回事?理解這塊邏輯,你需要了解以下幾個知識點:
wait()
:看到wait()
時,你可以簡單粗暴地認為每個物件都有一個類似於休息室的等待佇列,而wait()
正是把當前執行緒送進了等待佇列並暫停繼續執行;notify()
:如果說wait()
是把當前執行緒送進了等待佇列,那麼notify()
則是從等待佇列中取出執行緒。此外,和notify()
具有相似功能的還有個notifyAll()
。與notify()
不同的是,notifyAll()
會取出等待佇列中的所有執行緒;
看到這,你是不是覺得wait()
和notify()
簡直是完美的一對?其實不然。真相不僅不完美,還很不靠譜!
wait()
和notify()
在執行時都必須先獲得鎖,這也是你在程式碼中看到synchronized
的原因。notify()
在釋放鎖的時候,會從等待佇列中取出執行緒,此時的執行緒必須獲得鎖之後才能繼續執行。那麼,問題來了。如果佇列中有多個執行緒時,notify()
能取出指定的執行緒嗎?答案是不能!
換句話說,如果佇列中有多個執行緒,你將無法預料後續的執行結果!notifyAll()
雖然可以取出所有的執行緒,但最終也只能有一個執行緒能獲得鎖。
是不是有點懵?懵就對了。所以你看,wait()
和notify()
是不是很不靠譜?因此,如果你需要在專案程式碼中使用它們,請務必要小心謹慎!
此外,如果你閱讀過《Effective Java》,可以看到在這本書裡作者Josh Bloch也是強烈建議不要隨便使用這對組合。因為它們就像Java中的“組合語言”,確實複雜且不容易控制,如果有相似的併發場景需要處理,可以考慮使用Java中的其他高階的併發工具。
3. interrupt:做完這一單,我就退隱江湖
在王者的遊戲中,如果英雄血量沒了,可以回城補血。回城大概需要5秒左右,如果在回城的過程中,突然被攻擊或需要移位,那麼回城就會中斷。接下來,下面我們看看怎麼模擬回城中的中斷。
現在Player
中定義backHome()
方法用於回城。
public void backHome() {
System.out.println("回城中...");
synchronized (this) {
try {
this.wait();
System.out.println("已回城");
} catch (InterruptedException e) {
System.out.println("回城被中斷!");
}
}
}
接下來啟動新的執行緒呼叫backHome()
回城補血。
public static void main(String[] args) throws InterruptedException {
final Player neZha = new Player();
Thread neZhaBackHomeThread = new Thread() {
public void run() {
neZha.backHome();
}
};
neZhaBackHomeThread.start();
neZhaBackHomeThread.interrupt();
}
執行結果如下:
回城中...
回城被中斷!
Process finished with exit code 0
可以看到,回城被中斷了,因為我們呼叫了interrupt()
方法!那麼,線上程中的中斷是怎麼回事?往下看。
在Thread中,我們可以通過interrupt()
中斷執行緒。然而,如果你細心的話,還會發現Thread中除了interrupt()
方法之外,竟然還有兩個長相酷似的方法:interrupted()
和isInterrupted()
。這就要小心了。
interrupt()
:將執行緒設定為中斷狀態;interrupted()
:取消執行緒的中斷狀態;isInterrupted()
:判斷執行緒是否處於中斷狀態,而不會變更執行緒狀態。
不得不說,interrupt()
和interrupted()
這兩個方法的命名實在糟糕,你在編碼時可不要學習它,方法的名字應該清晰明瞭表達出其意圖。
那麼,當我們呼叫interrupt()
時,所呼叫物件的執行緒會立即丟擲InterruptedException
異常嗎?其實不然,這裡容易產生誤解。
interrupt()
方法只是改變了執行緒中的中斷狀態而已,並不會直接丟擲中斷異常。中斷異常的丟擲必須是當前執行緒在執行wait()
、sleep()
、join()
時才會丟擲。換句話說,如果當前執行緒正在處理其他的邏輯運算,不會被中斷,直到下次執行wait()
、sleep()
、join()
時!
4. join:稍等,等我結束你再開始
在前面的示例中,哪吒發起進攻和技能重新整理兩個執行緒是同時開始的。然而,我們在前面已經說了wait()
和notify()
並不靠譜,所以我們想在技能重新整理結束後再執行後續動作。
public static void main(String[] args) throws InterruptedException {
final Player neZha = new Player();
Thread neZhaFightThread = new Thread() {
public void run() {
neZha.fight();
}
};
Thread skillRefreshThread = new Thread() {
public void run() {
neZha.refreshSkills();
}
};
skillRefreshThread.start();
skillRefreshThread.join(); //這裡是重點
neZhaFightThread.start();
}
主執行緒呼叫join()
時,會阻塞當前執行緒繼續執行,直到目標執行緒中的任務執行完畢。此外,在呼叫join()
方法時,也可以設定超時時間。
小結
以上就是關於執行緒狀態及變遷的全部內容。在本文中,我們介紹了多執行緒的本質是協作,而狀態和動作方法是實現協作的方式。無論是面試還是其他的資料中,執行緒的狀態和方法都是重點。然而,我希望你明白了的是,對於本文知識點的掌握,不要從靜態的角度死記硬背,而是要動靜結合,從動態的方法認知靜態的狀態。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
在本文中,我們並沒有提到yield()
、Thread.sleep()
和Thread.current()
等方法。不過,如果你感興趣的話,不妨檢索資料:
- 瞭解
yield()
並對比它和join()
的不同; - 瞭解
wait()
並對比它和Thread.sleep()
的不同; - 瞭解
Thread.current()
的主要用法和它的實現。
延伸閱讀
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。