Object中的wait、notify、notifyAll,可以用於線程間的通信,核心原理為借助於監視器的入口集與等待集邏輯
通過這三個方法完成線程在指定鎖(監視器)上的等待與喚醒,這三個方法是以鎖(監視器)為中心的通信方法
除了他們之外,還有用於線程調度、控制的方法,他們是sleep、yield、join方法,他們可以用於線程的協作,他們是圍繞着線程的調度而來的
sleep方法
有兩個版本的sleep方法,看得出來,核心仍舊是native方法
非native方法只是進行了參數校驗,接着仍舊是調用的native方法,這個情形與wait是類似的
接下來仔細看下,native版本的sleep
在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和准確性的影響。該線程不丟失任何監視器的所屬權。
注意:
sleep不會釋放鎖,不會釋放鎖,不會釋放鎖
可以理解為他進入監視器這個房間之后,在這房間里面睡着了
與wait類似的是,sleep也是可中斷方法(從方法簽名可以看得出來,可能拋出InterruptedException),也就是說如果一個線程正在sleep,如果另外的線程將他中斷(調用interrupt方法),將會拋出異常,並且中斷狀態將會擦除
所以對於sleep方法,要么自己醒來,要么被中斷后也會醒來
對於sleep始終有一個超時時間的設置,所以,盡管他是在監視器內睡着了,但是並不會導致死鎖,因為他終究是要醒來的
如下,線程休眠500毫秒,主線程50毫秒打印一次狀態
ps:sleep方法的調用結果為狀態:TIMED_WAITING
借助於sleep方法,可以模擬線程的順序執行
比如下面示例,兩個階段,第二個階段將在第一個階段執行之后才會執行
package test1; import java.lang.Thread.State; public class T16 { public static void main(String[] args) { //模擬執行任務的第一個階段的執行 Thread stepOne = new Thread(() -> { System.out.println(Thread.currentThread().getName()+" : 第一階段任務開始執行"); try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+" : 第一階段任務執行結束"); } catch (InterruptedException e) { } }, "firstStage"); stepOne.start(); //模擬任務第二個階段的執行 Thread stepTwo = new Thread(() -> { while (!State.TERMINATED.equals(stepOne.getState())) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" : 我在等待第一階段任務執行結束"); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName()+" : 第二階段任務執行結束"); }, "secondStage"); stepTwo.start(); } }
另外,你應該已經注意到sleep方法都有static修飾,既然是靜態方法,在Thread中的慣例就是針對於:當前線程,當前線程,當前線程
yield方法
對於sleep或者wait方法,他們都將進入特定的狀態,伴隨着狀態的切換,也就意味着等待某些條件的發生,才能夠繼續,比如條件滿足,或者到時間等
但是yield方法不涉及這些事情,他針對的是時間片的划分與調度,所以對開發者來說只是臨時讓一下,讓一下他又不會死,就只是再等等
yield方法將會暫停當前正在執行的線程對象,並執行其他線程,他始終都是RUNNABLE狀態
不過要注意,可以認為yield只是一種建議性的,如果調用了yield方法,對CPU時間片的分配進行了“禮讓”,他仍舊有可能繼續獲得時間片,並且繼續執行
所以一次調用yield 並不一定會代表肯定會發生什么
借助於while循環以及yield方法,可以看得出來,也能一定程度上達到線程排序等待的效果
yield也是靜態方法,所以,也是針對於當前線程,當前線程,當前線程。
join方法
三個版本的join方法
方法的實現過程,與wait也是非常類似,下面兩個版本的方法一個調用join(0),一個參數校驗后,調用join(millis),所以根本還是單參數版本的join方法
在方法深入介紹前先看個例子
一個線程,循環5次,每次sleep 1s,主線程中打印信息
從結果可以看到,主線程總是在線程執行之后,才會執行,也就是主線程在等待我們創建的這個線程結束,結束了之后才會繼續進行
如果調整下順序--->start 與 join的先后順序,再次看下情況,可以發現順序沒有保障了
結論:
主線程main中調用啟動線程(調用start),然后調用該線程的join方法,可以達到主線程等待工作線程運行結束才執行的效果,並且join要在start調用后
如何做到的?
從上面源代碼可以看得出來,內部調用了wait方法,所以也能明白為啥join也會拋出InterruptedException了吧
主線程main中調用thread.join()方法,join方法相當於join(0),也就是
while (isAlive()) {
wait(0);
}
而這個wait(0)就相當於是this.wait(0),this就是我們自己創建的那個線程thread,看看方法的簽名是不是有一個synchronized
isAlive()也是this.isAlive(),也就是如果當前線程alive(已經啟動,但是未終止),那么將持續等待,等待的臨界資源就是我們創建的這個線程對象本身
所以這兩行代碼的含義就是:
該線程是否還存活?如果存活,調用join的那個線程將會在這個對象上進行等待(進入該線程對象的等待集)
也就是說調用一個線程的join方法,就是在這個線程是等待,這個線程對象就是我們的鎖對象(不要疑惑,Object都可以作為鎖,Thread實例對象怎么不可以?)
肯定大家很奇怪,既然是等待,wait又不會自己醒來,那不是出問題了嗎?
其實線程結束后,會調用this.notifyAll,所以主線程main會被喚醒
如果傳遞的參數不為0,將會走到下面的分支,會wait指定時長,與上面的邏輯一致,只不過是有指定超時時長而已
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
手動版本的等待結束
只是將join方法換成了同步代碼塊,鎖對象為那個線程的實例對象thread,調用他的wait方法
從結果上看,效果一樣
(不過此處沒有持續監測isAlive(),所以一旦主線程醒來,即使線程沒有結束,也會繼續,不能百分百確保main肯定等待線程結束)
不過要注意:注釋中有說明,自己不要使用Thread類的實例對象作為鎖對象,如果是現在這種場景,使用join即可
為什么?從我們目前來看,join方法就是以這個對象為鎖,如果你自己在使用,又是wait又是notify(notifyAll)的,萬一出現什么隱匿的問題咋辦?
所以join方法的原理就是:將指定的Thread實例對象作為鎖對象,在其上進行同步,只要那個線程還活着,那么就會持續等待(或者有限時長)
線程終止之后會調用自身this.notifyAll,以通知在其上等待的線程
簡單說,只要他活着大家就都等着, 他死了會通知,所以效果就是在哪里調用了誰的join,哪里就要等待這個線程結束,才能繼續
為什么要在start之后?
如上面所示,將join改造成同步代碼塊如下所示,如果這段同步代碼在start方法之前
看下結果,沒有等待指定線程結束,main主線程就結束了
因為如果還沒有調用start方法,那么isAlive是false(已開始未結束),主線程根本就不會等待,所以繼續執行,然后繼續到下面的start,然后主線程結束
所以,為什么join方法一定要在start之前?
就是因為這個isAlive方法的校驗,你沒有start,isAlive就是false,就不會同步等待,所以必須要先start,然后才能join
小結:
對於join方法,有兩個關鍵:
- 調用的哪個對象的join?
- 在哪里調用的?
換一個說法:
join的效果是:一個線程等待另一個線程(直到結束或者持續一段時間)才執行,那么誰等待誰?
join的效果是:一個線程等待另一個線程(直到結束或者持續一段時間)才執行,那么誰等待誰?
在哪個線程調用,哪個線程就會等待;調用的哪個Thread對象,就會等待哪個線程結束;
狀態圖回顧
在回顧下之前狀態一文中的切換圖,又了解了這幾個方法后,應該對狀態切換有了更全面的認識
總結
對於yield方法,比較容易理解,只是簡單地對於CPU時間片的“禮讓”,除非循環yield,否則一次yield,可能下次該線程仍舊可能會搶占到CPU時間片,可能方法調用和不調用沒差別
sleep是靜態方法,針對當前線程,進入休眠狀態,兩個版本的sleep方法始終有時間參數,所以必然會在指定的時間內蘇醒,他也不會釋放鎖,當然,sleep方法的調用非必須在同步方法(同步代碼塊)內
join是實例方法,表示等待誰,是用於線程順序的調度方法,可以做到一個線程等待另外一個線程,join有三個版本,指定超時時間或者持續等待直到目標線程執行結束,join也無需在同步方法(同步代碼塊)內
sleep和join都是可中斷方法,被其他線程中斷時,都會拋出InterruptedException異常,並且會醒來
join方法底層依賴wait,我們對比下wait與sleep
- wait和sleep都會使線程進入阻塞狀態,都是可中斷方法,被中斷后都會拋出異常
- wait是Object的方法,sleep是Thread的方法
- wait必須在同步中執行,sleep不需要(join底層依賴wait,但是不需要在同步中,因為join方法就是synchronized的)
- wait會釋放鎖,sleep不會釋放鎖
- wait(無超時設置的版本)會持續阻塞,必須等待喚醒,而sleep必然有超時,所以一定會自己醒來
- wait 實例方法(Object),在對象上調用,表示在其上等待;sleep靜態方法,當前線程

















