如何在 Java 中正確使用 wait, notify 和 notifyAll – 以生產者消費者模型爲例

wait, notify 和 notifyAll,這些在多線程中被常常用到的保留關鍵字,在實際開發的時候不少時候卻並無被你們重視。本文對這些關鍵字的使用進行了描述。java

在 Java 中能夠用 wait、notify 和 notifyAll 來實現線程間的通訊。舉個例子,若是你的Java程序中有兩個線程——即生產者和消費者,那麼生產者能夠通知消費者,讓消費者開始消耗數據,由於隊列緩衝區中有內容待消費(不爲空)。相應的,消費者能夠通知生產者能夠開始生成更多的數據,由於當它消耗掉某些數據後緩衝區再也不爲滿。程序員

咱們能夠利用wait()來讓一個線程在某些條件下暫停運行。例如,在生產者消費者模型中,生產者線程在緩衝區爲滿的時候,消費者在緩衝區爲空的時候,都應該暫停運行。若是某些線程在等待某些條件觸發,那當那些條件爲真時,你能夠用 notify 和 notifyAll 來通知那些等待中的線程從新開始運行。不一樣之處在於,notify 僅僅通知一個線程,而且咱們不知道哪一個線程會收到通知,然而 notifyAll 會通知全部等待中的線程。換言之,若是隻有一個線程在等待一個信號燈,notify和notifyAll都會通知到這個線程。但若是多個線程在等待這個信號燈,那麼notify只會通知到其中一個,而其它線程並不會收到任何通知,而notifyAll會喚醒全部等待中的線程。面試

在這篇文章中你將會學到如何使用 wait、notify 和 notifyAll 來實現線程間的通訊,從而解決生產者消費者問題。若是你想要更深刻地學習Java中的多線程同步問題,我強烈推薦閱讀Brian Goetz所著的《Java Concurrency in Practice | Java 併發實踐》,不讀這本書你的 Java 多線程征程就不完整哦!這是我最向Java開發者推薦的書之一。多線程

如何使用Wait

儘管關於wait和notify的概念很基礎,它們也都是Object類的函數,但用它們來寫代碼卻並不簡單。若是你在面試中讓應聘者來手寫代碼,用wait和notify解決生產者消費者問題,我幾乎能夠確定他們中的大多數都會無所適從或者犯下一些錯誤,例如在錯誤的地方使用 synchronized 關鍵詞,沒有對正確的對象使用wait,或者沒有遵循規範的代碼方法。說實話,這個問題對於不常使用它們的程序員來講確實使人感受比較頭疼。併發

第一個問題就是,咱們怎麼在代碼裏使用wait()呢?由於wait()並非Thread類下的函數,咱們並不能使用Thread.call()。事實上不少Java程序員都喜歡這麼寫,由於它們習慣了使用Thread.sleep(),因此他們會試圖使用wait() 來達成相同的目的,但很快他們就會發現這並不能順利解決問題。正確的方法是對在多線程間共享的那個Object來使用wait在生產者消費者問題中,這個共享的Object就是那個緩衝區隊列函數

第二個問題是,既然咱們應該在synchronized的函數或是對象裏調用wait,那哪一個對象應該被synchronized呢?答案是,那個你但願上鎖的對象就應該被synchronized,即那個在多個線程間被共享的對象。在生產者消費者問題中,應該被synchronized的就是那個緩衝區隊列。oop

永遠在循環(loop)裏調用 wait 和 notify,不是在 If 語句

如今你知道wait應該永遠在被synchronized的背景下和那個被多線程共享的對象上調用,下一個必定要記住的問題就是,你應該永遠在while循環,而不是if語句中調用wait。由於線程是在某些條件下等待的——在咱們的例子裏,即「若是緩衝區隊列是滿的話,那麼生產者線程應該等待」,你可能直覺就會寫一個if語句。但if語句存在一些微妙的小問題,致使即便條件沒被知足,你的線程你也有可能被錯誤地喚醒。因此若是你不在線程被喚醒後再次使用while循環檢查喚醒條件是否被知足,你的程序就有可能會出錯——例如在緩衝區爲滿的時候生產者繼續生成數據,或者緩衝區爲空的時候消費者開始消耗數據。因此記住,永遠在while循環而不是if語句中使用wait!我會推薦閱讀《Effective Java》,這是關於如何正確使用wait和notify的最好的參考資料。學習

基於以上認知,下面這個是使用wait和notify函數的規範代碼模板:ui

// The standard idiom for calling the wait method in Java 
synchronized (sharedObject) { 
    while (condition) { 
    sharedObject.wait(); 
        // (Releases lock, and reacquires on wakeup) 
    } 
    // do action based upon condition e.g. take or put into queue 
}

 

就像我以前說的同樣,在while循環裏使用wait的目的,是在線程被喚醒的先後都持續檢查條件是否被知足。若是條件並未改變,wait被調用以前notify的喚醒通知就來了,那麼這個線程並不能保證被喚醒,有可能會致使死鎖問題。spa

Java wait(), notify(), notifyAll() 範例

下面咱們提供一個使用wait和notify的範例程序。在這個程序裏,咱們使用了上文所述的一些代碼規範。咱們有兩個線程,分別名爲PRODUCER(生產者)和CONSUMER(消費者),他們分別繼承了了Producer和Consumer類,而Producer和Consumer都繼承了Thread類。Producer和Consumer想要實現的代碼邏輯都在run()函數內。Main線程開始了生產者和消費者線程,並聲明瞭一個LinkedList做爲緩衝區隊列(在Java中,LinkedList實現了隊列的接口)。生產者在無限循環中持續往LinkedList裏插入隨機整數直到LinkedList滿。咱們在while(queue.size == maxSize)循環語句中檢查這個條件。請注意到咱們在作這個檢查條件以前已經在隊列對象上使用了synchronized關鍵詞,於是其它線程不能在咱們檢查條件時改變這個隊列。若是隊列滿了,那麼PRODUCER線程會在CONSUMER線程消耗掉隊列裏的任意一個整數,並用notify來通知PRODUCER線程以前持續等待。在咱們的例子中,wait和notify都是使用在同一個共享對象上的。

原文代碼寫的跟屎同樣,我這裏本身寫了一個,運行無誤

public class ProducerConsumer {
	
	private int queuesize = 10;
	private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queuesize);
	
	public static void main(String[] args){
		
		ProducerConsumer pc = new ProducerConsumer();
		Consumer consumer = pc.new Consumer();
		Producer producer = pc.new Producer();
		
		producer.start();
		consumer.start();
	}
	
	
	class Consumer extends Thread{
		
		public void run(){
			consumer();
		}
		
		private void consumer(){
			while(true){
				synchronized (queue) {
					while(queue.size()==0){
						try {
							System.out.println("隊列爲空,等待數據");
							queue.wait();
						} catch (InterruptedException e) {
							// TODO: handle exception
							e.printStackTrace();
							queue.notify();
						}
					}
					queue.poll();
					queue.notify();
					System.out.println("從隊列取走一個元素,隊列剩餘"+queue.size()+"個元素");
				}
			}
		}
	}
	
	
	class Producer extends Thread{
		
		public void run(){
			produce();
		}
		
		private void produce(){
			while(true){
				synchronized (queue) {
					while(queue.size()==queuesize){
						try {
							System.out.println("隊列滿,等待有剩餘空間");
							queue.wait();
						} catch (InterruptedException e) {
							// TODO: handle exception
							e.printStackTrace();
							queue.notify();
						}
					}
					queue.offer(1);
					queue.notify();
					System.out.println("向隊列中插入一個元素,隊列剩餘空間:"+(queuesize-queue.size()));
				}
			}
		}
		
	}
}

本文重點:

1. 你可使用wait和notify函數來實現線程間通訊。你能夠用它們來實現多線程(>3)之間的通訊。

2. 永遠在synchronized的函數或對象裏使用wait、notify和notifyAll,否則Java虛擬機會生成 IllegalMonitorStateException。

3. 永遠在while循環裏而不是if語句下使用wait。這樣,循環會在線程睡眠先後都檢查wait的條件,並在條件實際上並未改變的狀況下處理喚醒通知。

4. 永遠在多線程間共享的對象(在生產者消費者模型裏即緩衝區隊列)上使用wait。

5. 基於前文說起的理由,更傾向用 notifyAll(),而不是 notify()。

相關文章
相關標籤/搜索