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

wait, notify 和 notifyAll,這些在多線程中被經常用到的保留關鍵字,在實際開發的時候很多時候卻並沒有被大家重視。本文對這些關鍵字的使用進行了描述。

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

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

如何使用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的就是那個緩衝區隊列。

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

現在你知道wait應該永遠在被synchronized的背景下和那個被多線程共享的對象上調用,下一個一定要記住的問題就是,你應該永遠在while循環,而不是if語句中調用wait。因爲線程是在某些條件下等待的——在我們的例子裏,即“如果緩衝區隊列是滿的話,那麼生產者線程應該等待”,你可能直覺就會寫一個if語句。但if語句存在一些微妙的小問題,導致即使條件沒被滿足,你的線程你也有可能被錯誤地喚醒。所以如果你不在線程被喚醒後再次使用while循環檢查喚醒條件是否被滿足,你的程序就有可能會出錯——例如在緩衝區爲滿的時候生產者繼續生成數據,或者緩衝區爲空的時候消費者開始小號數據。所以記住,永遠在while循環而不是if語句中使用wait!

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都是使用在同一個共享對象上的。

package com.niepengfei.test4;

import java.util.Queue;
import java.util.Random;

public class MyProcduer implements Runnable{

	private Queue<Integer> quene;
	
	private int maxSize;
	
	public MyProcduer(Queue<Integer> quene,int maxSize) {
		super();
		this.quene = quene;
		this.maxSize = maxSize;
	}

	@Override
	public void run() {
		while (true) {
			synchronized (quene) {
				while (quene.size() == maxSize) {
					System.out.println("緩衝區已經滿了,請消費者消費");
					try {
						quene.wait();//當前線程暫停,並釋放鎖
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				//產生一個隨進數,放進緩衝區
				Random random = new Random(); 
                int i = random.nextInt(); 
				quene.add(i);
				System.out.println("生產了數字:"+i);
				quene.notifyAll();
			}
		}
	}

}


package com.niepengfei.test4;

import java.util.Queue;

public class MyConsumer implements Runnable{

	private Queue<Integer> quene;
	
	private int maxSize;
	
	public MyConsumer(Queue<Integer> quene,int maxSize) {
		super();
		this.quene = quene;
		this.maxSize = maxSize;
	}
	
	@Override
	public void run() {
		while(true){
			synchronized (quene) {
				while (quene.isEmpty()) {
					try {
						System.out.println("緩衝區已經沒有數字了,請生產者生產");
						quene.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				
				System.out.println("消費者消費了:" + quene.remove());
				quene.notifyAll();
			}
		}
	}

}

package com.niepengfei.test4;

import java.util.LinkedList;
import java.util.Queue;

public class Test {
	
	public static void main(String[] args) {
		Queue<Integer> queue = new LinkedList<Integer>();
		
		MyProcduer procduer = new MyProcduer(queue, 10);
		MyConsumer consumer = new MyConsumer(queue, 10);
		
		new Thread(procduer).start();
		new Thread(consumer).start();
		
	}

}

本文重點:

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

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

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

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

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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章