你能說出多執行緒中 sleep、yield、join 的用法及 sleep與wait區別嗎?

點擊上方"IT牧場",選擇"設為星標"技術乾貨每日送達!

作者:noteless cnblogs.com/noteless/p/10443446.html

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的效果是:一個執行緒等待另一個執行緒(直到結束或者持續一段時間)才執行,那麼誰等待誰?

在哪個執行緒調用,哪個執行緒就會等待;調用的哪個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靜態方法,當前執行緒

乾貨分享

最近將個人學習筆記整理成冊,使用PDF分享。關注我,回復如下程式碼,即可獲得百度盤地址,無套路領取! •001:《Java並發與高並發解決方案》學習筆記;•002:《深入JVM內核——原理、診斷與優化》學習筆記;•003:《Java面試寶典》•004:《Docker開源書》•005:《Kubernetes開源書》•006:《DDD速成(領域驅動設計速成)》•007:全部•008:加技術討論群