歡迎來到《並發王者課》,本文是該系列文章中的第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()
的主要用法和它的實現。
延伸閱讀
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(盡量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。