Java中wait、notify、notifyAll使用詳解
基礎知識
首先我們需要知道,這幾個都是Object物件的方法。換言之,Java中所有的物件都有這些方法。
public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait() throws InterruptedException { wait(0); }
其中notify()、notifyAll()、wait(long timeout)是本地方法
其次我們需要知道這幾個方法主要是用來個執行緒 之間通訊的。那可能就有人會問,既然是用來執行緒之間通訊的,那為什麼這幾個方法不是線上程類Thread上呢?對於這個問題,我們先來看下這幾個方法的具體使用方式再來回答這個問題。
用法
Java中規定,在呼叫者三個方法時,當前執行緒必須獲得物件鎖。因此就得配合synchronized關鍵字來使用
//使用模式,不代表可執行程式碼 synchronized(object) { while(contidion) { object.wait(); } //object.notify(); //object.notifyAll(); }
或者
public synchronized void methodName() { while(contidion) { object.wait(); } //object.notify(); //object.notifyAll(); }
在synchronized拿到物件鎖之後,synchronized程式碼塊或者方法中,必定是會持有物件鎖的,因此就可以使用wait()或者notify()。
通過上述使用方法,我們也能很好理解為什麼這幾個方法是在Object上而不是在Thread上。因為每個物件都可以作為synchronized鎖的物件,因此wait、notify等必須和物件關聯才能配合synchronized使用。
作用
方法 | 作用 |
---|---|
wait | 執行緒自動釋放佔有的物件鎖,並等待notify。 |
notify | 隨機喚醒一個正在wait當前物件的執行緒,並讓被喚醒的執行緒拿到物件鎖 |
notifyAll | 喚醒所有正在wait當前物件的執行緒,但是被喚醒的執行緒會再次去競爭物件鎖。因為一次只有一個執行緒能拿到鎖,所有其他沒有拿到鎖的執行緒會被阻塞。推薦使用。 |
實際案例
接下來我們就使用wait()、notify()來實現一個生產者、消費者模式。這個也是面試過程中可能會被問到的地方。至於什麼是生產者消費者模式,不明白的同學請自行百度。
首先是一些基礎的程式碼
private static Boolean run = true;//控制是否生產和消費 private static final Integer MAX_CAPACITY = 5;//緩衝區最大數量 private static final LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();//緩衝佇列
生產者程式碼
/** * 生產者 */ class Producter extends Thread { @Override public void run() { while (run) { synchronized (queue) { while (queue.size() >= MAX_CAPACITY * 2) { try { System.out.println("緩衝佇列已滿,等待消費"); queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { String string = UUID.randomUUID().toString(); queue.put(string); System.out.println("生產:" + string); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } queue.notifyAll();//通知生產者和消費者 } } } }
消費者程式碼
/** * 消費者 */ class Consumer extends Thread { @Override public void run() { while (run) { synchronized (queue) { while (queue.isEmpty()) { try { System.out.println("佇列為空,等待生產"); queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { System.out.println("消費:" + queue.take()); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } queue.notifyAll();//通知生產者和消費者 } } } }
程式碼說明
1、生產者和消費者都繼承了執行緒Thread,因為wait、notify本身就是執行緒間通訊使用
2、生產者和消費者都有兩層while,外層的while是用來判斷是否執行生產者和消費者。記憶體的while用來判斷佇列queue是否已滿或者為空,如果滿足條件,則使得當前執行緒變成等待狀態(等待notify)。
3、內層的條件判斷為什麼用while不用if,原因是當執行緒被wait之後,會釋放物件鎖。當等待的執行緒被notify之後,必須再次嘗試去獲取物件鎖,如果沒有獲取到物件鎖,那還必須等待,直到拿到物件鎖之後才能向後執行。
4、當生產者生產了一個數據或者消費者消費了一個數據之後,使用notifyAll()方法來通知所有等待當前物件鎖的執行緒,但是一次只會有一個等待的執行緒能拿到鎖。
程式碼執行結果
佇列為空,等待生產 生產:e422484e-8eb3-4a91-8a7c-97e21d7ef498 生產:7894b802-2529-4798-ba98-9f0657f39240 生產:848f6759-a427-4a94-89dc-3f484daaa467 生產:f711d3dc-972c-4c44-8640-faffe376d354 生產:38a08e62-d774-4ed5-8b51-f1ad534c246f 消費:e422484e-8eb3-4a91-8a7c-97e21d7ef498 消費:7894b802-2529-4798-ba98-9f0657f39240 消費:848f6759-a427-4a94-89dc-3f484daaa467 消費:f711d3dc-972c-4c44-8640-faffe376d354 消費:38a08e62-d774-4ed5-8b51-f1ad534c246f 佇列為空,等待生產 生產:9fae26f3-0b6e-4fbd-9620-040667efe0af 生產:95bb1d88-e08a-4f70-a270-f75760994184 消費:9fae26f3-0b6e-4fbd-9620-040667efe0af 消費:95bb1d88-e08a-4f70-a270-f75760994184 佇列為空,等待生產 生產:13d304bc-dff3-44a4-9527-2e0facd884e7 生產:2693e069-bae1-4beb-adcd-a3c3bf5d232b 消費:13d304bc-dff3-44a4-9527-2e0facd884e7 消費:2693e069-bae1-4beb-adcd-a3c3bf5d232b
由結果也可以看出來,生產和消費是無規則的,因為雖然notifyAll()通知了所有的等待執行緒,但是不確定那個執行緒中能拿到物件鎖。但是也有一個很明顯的問題,因為同時只有一個執行緒能拿到物件鎖,生產者和消費者不可能同時執行。