等待通知吧!


wait()、notify()與notifyAll()是由 Object所提供的方法,在定義自己的類別時會繼承下來,在Object類別中,wait()、notify()與 notifyAll()都被宣告為final,所以無法在繼承之後重新定義它們,你可以透過這三個方法控制執行緒釋放物件的鎖定旗標或者通知參與鎖定旗標的競爭。


當執行緒要進入synchronized範圍,會先取得所指定物件的鎖定旗標,在執行synchronized的程式碼期間,如果呼叫鎖定中物件的wait()方法,則該執行緒會被放入物件的「等待集合」(Wait set)中, 執行緒會釋放物件的鎖定旗標,其它的執行緒則可以競爭鎖定旗標,取得鎖定旗標的執行緒可以執行synchronized範圍的程式碼。

被放在等待集中的執行緒將不參與執行緒的排班,wait()可 以指定等待的時間,如果指定時間的話,則時間到之後執行緒會再度加入排班,如果指定時間0或不指定,則執行緒會持續等待,直到有被中斷(interrupt)或是被告知(notify)可以參與排班。

當被競爭鎖定旗標的物件之notify()被呼叫時,它會從物件的等待集中選出「一個」執行緒加入排班,被選出的執行緒是隨機的,被選出的執行緒會與其它正在執行的執行緒共 同競爭對物件的鎖定旗標;如果你呼叫notifyAll(),則「所有」在等待集中的執行緒都會被喚醒,這些執行緒會與其它正在執行的執行緒共同競爭對物件的 鎖定。

簡單的說,當執行緒呼叫到物件的wait()方法時,表示它要先讓出synchronized區塊的使用權並等待通知,或是等待一段指定的時間,直到被通知或時間到時再從 等待點開始執行,這就好比你要叫某人作事,作到一半時某人叫你等候通知(或等候1分鐘之類的),當你被通知(或時間到時)某人會繼續為你服務。

說明wait()、notify()或notifyAll()的應用最常見的一個例子,就是生產者(Producer)與消費者(Consumer)的 例子,如果生產者會將產品交給店員,而消費者從店員處取走產品,店員一次只能持有固定數量產品,如果生產者生產了過多的產品,店員叫生產者等一下 (wait),如果店中有空位放產品了再通知(notify)生產者繼續生產,如果店中沒有產品了,店員會告訴消費者等一下(wait),如果店中有產品 了再通知(notify)消費者來取走產品。

以下舉一個最簡單的:生產者每次生產一個int整數交給店員,而消費者從店員處取走整數,店員一次只能持有一個整數。

以程式實例來看,首先是生產者:
  • Producer.java
public class Producer implements Runnable {
private Clerk clerk;

public Producer(Clerk clerk) {
this.clerk = clerk;
}

public void run() {
System.out.println(
"生產者開始生產整數......");

// 生產1到10的整數
for(int product = 1; product <= 10; product++) {
try {
// 暫停隨機時間
Thread.sleep((int) (Math.random() * 3000));
}
catch(InterruptedException e) {
e.printStackTrace();
}
// 將產品交給店員
clerk.setProduct(product);
}
}
}

再來是消費者:
  • Consumer.java
public class Consumer implements Runnable {
private Clerk clerk;

public Consumer(Clerk clerk) {
this.clerk = clerk;
}

public void run() {
System.out.println(
"消費者開始消耗整數......");

// 消耗10個整數
for(int i = 1; i <= 10; i++) {
try {
// 等待隨機時間
Thread.sleep((int) (Math.random() * 3000));
}
catch(InterruptedException e) {
e.printStackTrace();
}

// 從店員處取走整數
clerk.getProduct();
}
}
}

生產者將產品放至店員,而消費者從店員處取走產品,所以店員來決定誰必須等待並等候通知。
  • Clerk.java
public class Clerk {
// -1 表示目前沒有產品
private int product = -1;

// 這個方法由生產者呼叫
public synchronized void setProduct(int product) {
while(this.product != -1) {
try {
// 目前店員沒有空間收產品,請稍候!
wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}

this.product = product;
System.out.printf("生產者設定 (%d)%n", this.product);

// 通知等待區中的一個消費者可以繼續工作了
notify();
}

// 這個方法由消費者呼叫
public synchronized int getProduct() {
while(this.product == -1) {
try {
// 缺貨了,請稍候!
wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}

int p = this.product;
System.out.printf(
"消費者取走 (%d)%n", this.product);
this.product = -1;

// 通知等待區中的一個生產者可以繼續工作了
notify();

return p;
}
}

根據 規格書中所說明 ,執行緒也有可能在未經notify()、interrupt()或逾時的情況下自動甦醒(spurious wakeup),雖然這種情況實務上很少發生,但應用程式應考量這種情況,你必須持續檢測這種情況,因而wait()必須總是在迴圈中執行,例如:
     synchronized (obj) {
         while (執行條件不成立時)
             obj.wait(timeout);
         ... // 執行一些動作進行判斷
     }

使用這麼一個程式來測試:
  • Main.java
public class Main {
public static void main(String[] args) {
Clerk clerk = new Clerk();

Thread producerThread = new Thread(new Producer(clerk));
Thread consumerThread = new Thread(new Consumer(clerk));

producerThread.start();
consumerThread.start();
}
}

生產者會生產10個整數,而消費者會消耗10個整數,由於店員處只能放置一個整數,所以每生產一個就消耗一個,其結果如上所示是無誤的。

如果一個執行緒進入物件的等待集中,你可以中斷它的等待,這時將會發生InterruptedException例外物件,interrupt()方法可用來進行這項工作。