1. 程式人生 > >探索JAVA併發 - 終於搞懂了sleep/wait/notify/notifyAll

探索JAVA併發 - 終於搞懂了sleep/wait/notify/notifyAll

> sleep/wait/notify/notifyAll分別有什麼作用?它們的區別是什麼?wait時為什麼要放在迴圈裡而不能直接用if? ## 簡介 首先對幾個相關的方法做個簡單解釋,Object中有幾個用於執行緒同步的方法:wait、notify、notifyAll。 ```java public class Object { public final native void wait(long timeout) throws InterruptedException; public final native void notify(); public final native void notifyAll(); } ``` + wait: 釋放當前鎖,阻塞直到被notify或notifyAll喚醒,或者超時,或者執行緒被中斷(InterruptedException) + notify: 任意選擇一個(無法控制選哪個)正在這個物件上等待的執行緒把它喚醒,其它執行緒依然在等待被喚醒 + notifyAll: 喚醒所有執行緒,讓它們去競爭,不過也只有一個能搶到鎖 + sleep: 不是Object中的方法,而是Thread類的靜態方法,讓當前執行緒持有鎖阻塞指定時間 ## sleep和wait sleep和wait都可以讓執行緒阻塞,也都可以指定超時時間,甚至還都會丟擲中斷異常InterruptedException。 而它們最大的區別就在於,sleep時執行緒依然持有鎖,別人無法進當前同步方法;wait時放棄了持有的鎖,其它執行緒有機會進入該同步方法。多次提到同步方法,因為wait必須在synchronized同步程式碼塊中,否則會丟擲異常IllegalMonitorStateException,notify也是如此,可以說wait和notify是就是為了在同步程式碼中做執行緒排程而生的。 下面一個簡單的例子展現sleep和wait的區別: ```java import java.util.Date; import java.util.concurrent.atomic.AtomicInteger; public class Main { // 日誌行號記錄 private AtomicInteger count = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Main main = new Main(); // 開啟兩個執行緒去執行test方法 new Thread(main::test).start(); new Thread(main::test).start(); } private synchronized void test() { try { log("進入了同步方法,並開始睡覺,1s"); // sleep不會釋放鎖,因此其他執行緒不能進入這個方法 Thread.sleep(1000); log("睡好了,但沒事做,有事叫我,等待2s"); //阻塞在此,並且釋放鎖,其它執行緒可以進入這個方法 //當其它執行緒呼叫此物件的notify或者notifyAll時才有機會停止阻塞 //就算沒有人notify,如果超時了也會停止阻塞 wait(2000); log("我要走了,但我要再睡一覺,10s"); //這裡睡的時間很長,因為沒有釋放鎖,其它執行緒就算wait超時了也無法繼續執行 Thread.sleep(10000); log("走了"); notify(); } catch (InterruptedException e) { e.printStackTrace(); } } // 列印日誌 private void log(String s) { System.out.println(count.incrementAndGet() + " " + new Date().toString().split(" ")[3] + "\t" + Thread.currentThread().getName() + " " + s); } } /* 輸出: 1 00:13:23 Thread-0 進入了同步方法,並開始睡覺,1s 2 00:13:24 Thread-0 睡好了,但沒事做,有事叫我,等待2s 3 00:13:24 Thread-1 進入了同步方法,並開始睡覺,1s 4 00:13:25 Thread-1 睡好了,但沒事做,有事叫我,等待2s 5 00:13:26 Thread-0 我要走了,但我要再睡一覺,10s 6 00:13:36 Thread-0 走了 7 00:13:36 Thread-1 我要走了,但我要再睡一覺,10s 8 00:13:46 Thread-1 走了 */ ``` 對輸出做個簡單解釋(已經看懂程式碼的童鞋可以跳過): ``` 1 00:13:23 Thread-0 進入了同步方法,並開始睡覺,1s // Thread-0首先進入同步方法,Thread-1只能門外候著 2 00:13:24 Thread-0 睡好了,但沒事做,有事叫我,等待2s // Thread-0 sleep 1秒這段時間,Thread-1沒進來,證明sleep沒有釋放鎖 3 00:13:24 Thread-1 進入了同步方法,並開始睡覺,1s // Thread-0開始wait後Thread-1馬上就進來了,證明wait釋放了鎖 4 00:13:25 Thread-1 睡好了,但沒事做,有事叫我,等待2s // Thread-1也打算wait 2秒(2秒後真的能醒來嗎?) 5 00:13:26 Thread-0 我要走了,但我要再睡一覺,10s // Thread-0已經wait超時醒來了,這次準備sleep 10s 6 00:13:36 Thread-0 走了 // 10s過去了Thread-0都sleep結束了,那個說要wait 2s的Thread-1還沒動靜,證明超時也沒用,還得搶到鎖 7 00:13:36 Thread-1 我要走了,但我要再睡一覺,10s // Thread-0退出同步程式碼後,Thread-1才終於得到了鎖,能行動了 8 00:13:46 Thread-1 走了 ``` ## notify和notifyAll 同樣是喚醒等待的執行緒,同樣最多隻有一個執行緒能獲得鎖,同樣不能控制哪個執行緒獲得鎖。 區別在於: + notify:喚醒一個執行緒,其他執行緒依然處於wait的等待喚醒狀態,如果被喚醒的執行緒結束時沒呼叫notify,其他執行緒就永遠沒人去喚醒,只能等待超時,或者被中斷 + notifyAll:所有執行緒退出wait的狀態,開始競爭鎖,但只有一個執行緒能搶到,這個執行緒執行完後,其他執行緒又會有一個幸運兒脫穎而出得到鎖 如果覺得解釋的不夠明白,程式碼來一波: ```java import java.util.Date; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; public class Main { private AtomicInteger count = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Main main = new Main(); // 開啟兩個執行緒去執行test方法 for (int i = 0; i < 10; i++) { new Thread(main::testWait).start(); } Thread.sleep(1000); for (int i = 0; i < 5; i++) { main.testNotify(); } } private synchronized void testWait() { try { log("進入了同步方法,開始wait"); wait(); log("wait結束"); } catch (InterruptedException e) { e.printStackTrace(); } } private synchronized void testNotify() { notify(); } private void log(String s) { System.out.println(count.incrementAndGet() + " " + new Date().toString().split(" ")[3] + "\t" + Thread.currentThread().getName() + " " + s); } } /* 輸出: 1 00:59:32 Thread-0 進入了同步方法,開始wait 2 00:59:32 Thread-9 進入了同步方法,開始wait 3 00:59:32 Thread-8 進入了同步方法,開始wait 4 00:59:32 Thread-7 進入了同步方法,開始wait 5 00:59:32 Thread-6 進入了同步方法,開始wait 6 00:59:32 Thread-5 進入了同步方法,開始wait 7 00:59:32 Thread-4 進入了同步方法,開始wait 8 00:59:32 Thread-3 進入了同步方法,開始wait 9 00:59:32 Thread-2 進入了同步方法,開始wait 10 00:59:32 Thread-1 進入了同步方法,開始wait 11 00:59:33 Thread-0 wait結束 12 00:59:33 Thread-6 wait結束 13 00:59:33 Thread-7 wait結束 14 00:59:33 Thread-8 wait結束 15 00:59:33 Thread-9 wait結束 */ ``` 例子中有10個執行緒在wait,但notify了5次,然後其它執行緒一直阻塞,這也就說明使用notify時如果不能準確控制和wait的執行緒數對應,可能會導致某些執行緒永遠阻塞。 使用notifyAll喚醒所有等待的執行緒: ```java import java.util.Date; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; public class Main { private AtomicInteger count = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Main main = new Main(); // 開啟兩個執行緒去執行test方法 for (int i = 0; i < 5; i++) { new Thread(main::testWait).start(); } Thread.sleep(1000); main.testNotifyAll(); } private synchronized void testWait() { try { log("進入了同步方法,開始wait"); wait(); log("wait結束"); } catch (InterruptedException e) { e.printStackTrace(); } } private synchronized void testNotifyAll() { notifyAll(); } private void log(String s) { System.out.println(count.incrementAndGet() + " " + new Date().toString().split(" ")[3] + "\t" + Thread.currentThread().getName() + " " + s); } } /* 輸出: 1 01:03:24 Thread-0 進入了同步方法,開始wait 2 01:03:24 Thread-4 進入了同步方法,開始wait 3 01:03:24 Thread-3 進入了同步方法,開始wait 4 01:03:24 Thread-2 進入了同步方法,開始wait 5 01:03:24 Thread-1 進入了同步方法,開始wait 6 01:03:25 Thread-1 wait結束 7 01:03:25 Thread-2 wait結束 8 01:03:25 Thread-3 wait結束 9 01:03:25 Thread-4 wait結束 10 01:03:25 Thread-0 wait結束 */ ``` 只需要呼叫一次notifyAll,所有的等待執行緒都被喚醒,並且去競爭鎖,然後依次(無序)獲取鎖完成了後續任務。 ## 為什麼wait要放到迴圈中使用 一些原始碼中出現wait時,往往都是伴隨著一個迴圈語句出現的,比如: ```java private synchronized void f() throws InterruptedException { while (!isOk()) { wait(); } System.out.println("I'm ok"); } ``` 既然wait會被阻塞直到被喚醒,那麼用if+wait不就可以了嗎?其他執行緒發現條件達到時notify一下不就行了? 理想情況確實如此,但實際開發中我們往往不能保證這個執行緒被notify時條件已經滿足了,因為很可能有某個無關(和這個條件的邏輯無關)的執行緒因為需要執行緒排程而呼叫了notify或者notifyAll。此時如果樣例中位置等待的執行緒不巧被喚醒,它就會繼續往下執行,但因為用的if,這次被喚醒就不會再判斷條件是否滿足,最終程式按照我們不期望的方式執行下去。 ![微信公眾號“一杯82年的JAVA”](https://img2018.cnblogs.com/blog/1031208/201909/1031208-20190906011558358-1333917