sleep
sleep方法是在Thread類中的一個靜態方法,當一個線程調用了sleep方法,被調用的那個線程就會暫時的讓出指定時間的CPU執行權,在這段時間也不會參與CPU的調度,當時間到了之后,就會重新回到就緒狀態,等待CPU的再次調度,注意是就緒狀態,而不是重新拿回CPU的執行權。並且,在休眠期間,只是會讓出CPU的執行權,但是之前獲得的鎖資源,還是繼續持有,等CPU調度到該線程重新獲取到執行權的時候,就會繼續運行sleep之后的代碼。接下來用一個例子來說明Sleep期間不會放棄鎖資源
public class SleepDemo {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread one = new Thread(() -> {
lock.lock();
System.out.println("線程A准備被Sleep"); //1
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程A被喚醒"); //2
lock.unlock();
});
Thread two = new Thread(() -> {
lock.lock();
System.out.println("線程B准備被Sleep"); //3
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程B被喚醒"); //4
lock.unlock();
});
one.start();
two.start();
}
}
在上面的代碼中,new了兩個線程,但是只有一個鎖資源lock,如果sleep期間會釋放鎖資源的話,那么輸出結果應當是交替輸出的,因為在執行代碼1的時候,會被剝奪執行權,那么線程2將會拿到鎖,但是事實真的是這樣嗎,看輸出結果
線程A准備被Sleep
線程A被喚醒
線程B准備被Sleep
線程B被喚醒
可以看到,線程A被sleep的時候,線程2執行,想要獲取鎖I資源lock,但是發現lock被占有了,只好阻塞,然后線程1的休眠時間到了,重新獲取執行權,繼續執行sleep之后的代碼,然后釋放鎖,然后線程2拿到鎖,執行后續代碼。通過上面的代碼就可以看出,sleep期間是不會釋放鎖資源的,只是暫時的讓出了cpu的執行權。
yield
yield和sleep在功能上有點像。yield方法是Thread中的一個靜態方法,當一個線程調用yield方法的時候,則會直接讓出自己cpu執行權,並且直接到就緒狀態,也就是說,調用yield不會讓線程阻塞。舉個通俗點的例子,我們都知道,操作系統調度線程的時候,會有一個時間片機制,當時間片用完之后,就會回到就緒狀態等待cpu的下一次調度,如果A線程在他的時間片還沒用完之前,調用了yield方法,就代表着,放棄還未用完的時間片,告訴線程調度器,還有多的時間片我不想用了,你直接進行下一輪調度吧,然后A線程也回到了就緒狀態。請注意,調用了yield的線程,是回到了就緒狀態,而不是阻塞
,這意味着,即便他剛調用玩yield方法,放棄了自己的執行權,但是在接下來的線程調度中,仍然還有可能獲得cpu的執行權。
public class YieldDemo1 {
public static void main(String[] args) {
Thread one = new Thread(() -> {
for (int i = 0; i < 20; i++) {
if (i%5 == 0){
System.out.println(Thread.currentThread().getName()+"調用yield放棄了cpu執行權");
Thread.yield();
}
}
System.out.println(Thread.currentThread().getName()+"結束");
},"線程1");
Thread two = new Thread(() -> {
for (int i = 0; i < 20; i++) {
if (i%5 == 0){
System.out.println(Thread.currentThread().getName()+"調用yield放棄了cpu執行權");
Thread.yield();
}
}
System.out.println(Thread.currentThread().getName()+"結束");
},"線程2");
one.start();
two.start();
}
}
輸出結果
線程1調用yield放棄了cpu執行權
線程2調用yield放棄了cpu執行權
線程1調用yield放棄了cpu執行權
線程1調用yield放棄了cpu執行權
線程1調用yield放棄了cpu執行權
線程2調用yield放棄了cpu執行權
線程1結束
線程2調用yield放棄了cpu執行權
線程2調用yield放棄了cpu執行權
線程2結束
可以看看第三、四、五行輸出,在線程1放棄了cpu執行權,繼續調度的時候,下一次調度還是調度到了線程1.這也驗證了我們上面的說法。
- 總結:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態。而yield和sleep的區別在於,調用了sleep方法之后,
當前調用線程將會被阻塞指定時間,在這期間不會參與調度
,而yield只是放棄了自己的時間片執行權,並不會被阻塞,直接以就緒狀態去參與下一次的線程調度
,下次調度還是有可能會被調度到。
join
在我們平常的編程里,經常會碰到這樣的一個場景,例如,我們有A,B,C三種線程,其中A類線程是用於讀數據的,然后B類線程是處理數據的,把A類線程讀的數據進行匯總,C線程是寫入數據的,需要用到A線程讀取的數據,並且要等數據全部讀取完畢,也就是A線程結束了,B線程才開始處理數據,等B類線程全部處理完畢之后,C類線程才能寫入數據。這個場景很常見,多線程加載資源,加載完畢再統一匯總處理。
以上這種場景,如果我們需要自己手動控制判斷所有的線程結束了才執行下面的任務,其實是比較麻煩的,因此Thread提供了一個join方法,可以阻塞當前正在運行的線程,讓調用join的線程運行完畢之后,當前線程才能繼續運行。看代碼就明白了
public class join {
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(() ->{
try {
System.out.println("線程1");
Thread.sleep(1000);
for (;;);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread two = new Thread(() ->{
try {
System.out.println("線程2");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
one.start();
two.start();
one.join();
two.join();
}
}
上面這段代碼中,首先啟動第一個線程,輸出線程1
然后休眠,線程2獲取執行權,打印線程2
,之后線程2休眠,線程1獲取執行權,然后由於線程1調用了join方法,所以此時主線程阻塞,因為要等線程1執行完畢之后,主線程才會繼續執行,但是線程1是個死循環,永遠不會結束,所以,這段程序將不會停止,也就是主線程將一直阻塞,two.join也沒有執行的機會。
同理,如果A線程中調用B線程的join方法,那么A線程將會被暫時阻塞,直到B線程執行完畢,A線程才會繼續,其實對於join,就有點像把多個線程合並成一個單線程來處理