說說多線程 sleep、yield、join、sleep、wait 區別


說說多線程 sleep、yield、join、sleep、wait 區別


作者|noteless

cnblogs.com/noteless/p/10443446.html

前言

Object中的wait、notify、notifyAll,可以用於線程間的通信,核心原理為藉助於監視器的入口集與等待集邏輯。

通過這三個方法完成線程在指定鎖(監視器)上的等待與喚醒,這三個方法是以鎖(監視器)為中心的通信方法。

除了他們之外,還有用於線程調度、控制的方法,他們是sleep、yield、join方法,他們可以用於線程的協作,他們是圍繞著線程的調度而來的。

sleep方法

有兩個版本的sleep方法,看得出來,核心仍舊是native方法,非native方法只是進行了參數校驗,接著仍舊是調用的native方法,這個情形與wait是類似的。

說說多線程 sleep、yield、join、sleep、wait 區別

接下來仔細看下,native版本的sleep

在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響。該線程不丟失任何監視器的所屬權。

注意

sleep不會釋放鎖,不會釋放鎖,不會釋放鎖,可以理解為他進入監視器這個房間之後,在這房間裡面睡著了。

與wait類似的是,sleep也是可中斷方法(從方法簽名可以看得出來,可能拋出InterruptedException),也就是說如果一個線程正在sleep,如果另外的線程將他中斷(調用interrupt方法),將會拋出異常,並且中斷狀態將會擦除。

所以對於sleep方法,要麼自己醒來,要麼被中斷後也會醒來。

對於sleep始終有一個超時時間的設置,所以,儘管他是在監視器內睡著了,但是並不會導致死鎖,因為他終究是要醒來的。

如下,線程休眠500毫秒,主線程50毫秒打印一次狀態

ps:sleep方法的調用結果為狀態:TIMED_WAITING

說說多線程 sleep、yield、join、sleep、wait 區別

藉助於sleep方法,可以模擬線程的順序執行

比如下面示例,兩個階段,第二個階段將在第一個階段執行之後才會執行。

<code>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();
}
}/<code>
說說多線程 sleep、yield、join、sleep、wait 區別

另外,你應該已經注意到sleep方法都有static修飾,既然是靜態方法,在Thread中的慣例就是針對於:當前線程,當前線程,當前線程

yield方法

對於sleep或者wait方法,他們都將進入特定的狀態,伴隨著狀態的切換,也就意味著等待某些條件的發生,才能夠繼續,比如條件滿足,或者到時間等

但是yield方法不涉及這些事情,他針對的是時間片的劃分與調度,所以對開發者來說只是臨時讓一下,讓一下他又不會死,就只是再等等。

yield方法將會暫停當前正在執行的線程對象,並執行其他線程,他始終都是RUNNABLE狀態。

說說多線程 sleep、yield、join、sleep、wait 區別

不過要注意,可以認為yield只是一種建議性的,如果調用了yield方法,對CPU時間片的分配進行了“禮讓”,他仍舊有可能繼續獲得時間片,並且繼續執行。

所以一次調用yield 並不一定會代表肯定會發生什麼。

說說多線程 sleep、yield、join、sleep、wait 區別

藉助於while循環以及yield方法,可以看得出來,也能一定程度上達到線程排序等待的效果。

說說多線程 sleep、yield、join、sleep、wait 區別

yield也是靜態方法,所以,也是針對於當前線程,當前線程,當前線程。


join方法

三個版本的join方法

說說多線程 sleep、yield、join、sleep、wait 區別

方法的實現過程,與wait也是非常類似,下面兩個版本的方法一個調用join(0),一個參數校驗後,調用join(millis),所以根本還是單參數版本的join方法。

說說多線程 sleep、yield、join、sleep、wait 區別

在方法深入介紹前先看個例子。

一個線程,循環5次,每次sleep 1s,主線程中打印信息

從結果可以看到,主線程總是在線程執行之後,才會執行,也就是主線程在等待我們創建的這個線程結束,結束了之後才會繼續進行。

說說多線程 sleep、yield、join、sleep、wait 區別

說說多線程 sleep、yield、join、sleep、wait 區別

如果調整下順序--->start 與 join的先後順序,再次看下情況,可以發現順序沒有保障了。

說說多線程 sleep、yield、join、sleep、wait 區別

結論

主線程main中調用啟動線程(調用start),然後調用該線程的join方法,可以達到主線程等待工作線程運行結束才執行的效果,並且join要在start調用後

如何做到的?

說說多線程 sleep、yield、join、sleep、wait 區別

從上面源代碼可以看得出來,內部調用了wait方法,所以也能明白為啥join也會拋出InterruptedException了吧

主線程main中調用thread.join()方法,join方法相當於join(0),也就是

<code>while (isAlive()) {
   wait(0);
}/<code>

而這個wait(0)就相當於是this.wait(0),this就是我們自己創建的那個線程thread,看看方法的簽名是不是有一個synchronized

isAlive()也是this.isAlive(),也就是如果當前線程alive(已經啟動,但是未終止),那麼將持續等待,等待的臨界資源就是我們創建的這個線程對象本身。

所以這兩行代碼的含義就是:

該線程是否還存活?如果存活,調用join的那個線程將會在這個對象上進行等待(進入該線程對象的等待集)

也就是說調用一個線程的join方法,就是在這個線程是等待,這個線程對象就是我們的鎖對象(不要疑惑,Object都可以作為鎖,Thread實例對象怎麼不可以?)

肯定大家很奇怪,既然是等待,wait又不會自己醒來,那不是出問題了嗎?

其實線程結束後,會調用this.notifyAll,所以主線程main會被喚醒。

如果傳遞的參數不為0,將會走到下面的分支,會wait指定時長,與上面的邏輯一致,只不過是有指定超時時長而已。

<code>long delay = millis - now;
if (delay <= 0) {
   break;
  }
wait(delay);
now = System.currentTimeMillis() - base;/<code>

手動版本的等待結束,只是將join方法換成了同步代碼塊,鎖對象為那個線程的實例對象thread,調用他的wait方法。

從結果上看,效果一樣(不過此處沒有持續監測isAlive(),所以一旦主線程醒來,即使線程沒有結束,也會繼續,不能百分百確保main肯定等待線程結束)。

說說多線程 sleep、yield、join、sleep、wait 區別

不過要注意:註釋中有說明,自己不要使用Thread類的實例對象作為鎖對象,如果是現在這種場景,使用join即可。

為什麼?從我們目前來看,join方法就是以這個對象為鎖,如果你自己在使用,又是wait又是notify(notifyAll)的,萬一出現什麼隱匿的問題咋辦?

說說多線程 sleep、yield、join、sleep、wait 區別

所以join方法的原理就是:將指定的Thread實例對象作為鎖對象,在其上進行同步,只要那個線程還活著,那麼就會持續等待(或者有限時長)

線程終止之後會調用自身this.notifyAll,以通知在其上等待的線程。

簡單說,只要他活著大家就都等著, 他死了會通知,所以效果就是在哪裡調用了誰的join,哪裡就要等待這個線程結束,才能繼續。

為什麼要在start之後?

說說多線程 sleep、yield、join、sleep、wait 區別

如上面所示,將join改造成同步代碼塊如下所示,如果這段同步代碼在start方法之前。

看下結果,沒有等待指定線程結束,main主線程就結束了。

說說多線程 sleep、yield、join、sleep、wait 區別

因為如果還沒有調用start方法,那麼isAlive是false(已開始未結束),主線程根本就不會等待,所以繼續執行,然後繼續到下面的start,然後主線程結束

所以,為什麼join方法一定要在start之前?

就是因為這個isAlive方法的校驗,你沒有start,isAlive就是false,就不會同步等待,所以必須要先start,然後才能join

小結

對於join方法,有兩個關鍵:

  • 調用的哪個對象的join?
  • 在哪裡調用的?

換一個說法:

join的效果是:一個線程等待另一個線程(直到結束或者持續一段時間)才執行,那麼誰等待誰?

在哪個線程調用,哪個線程就會等待;調用的哪個Thread對象,就會等待哪個線程結束;

狀態圖回顧

在回顧下之前狀態一文中的切換圖,又瞭解了這幾個方法後,應該對狀態切換有了更全面的認識

說說多線程 sleep、yield、join、sleep、wait 區別

總結

對於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靜態方法,當前線程


分享到:


相關文章: