2015-02-22

[轉載] Java Gossip: wait()、notify()

轉載自:Java Gossip: wait()、notify()

wait()notify()notifyAll() 是由 Object 所提供的方法,您在定義自己的類別時會繼承下來(記得 Java 中所有的物件最頂層都繼承自 Object),wait()notify() notifyAll() 都被宣告為 "final",所以您無法重新定義它們,透過這三個方法您可以控制執行緒是否為 Runnable 狀態。

您必須在同步化的方法或區塊中呼叫 wait() 方法,當物件的 wait() 方法被調用,目前的執行緒會被放入物件的「等待集合」(Wait set)中, 執行緒會釋放物件的鎖定,其它的執行緒可以競爭鎖定,取得鎖定的執行緒可以執行同步化區塊;被放在等待集中的執行緒將不參與執行緒的排班,wait() 可 以指定等待的時間,如果指定時間的話,則時間到之後執行緒會再度加入排班,如果指定時間 0 或不指定,則執行緒會持續等待,直到有被中斷(interrupt)或是被告知(notify)可以參與排班。

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

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

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


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


以程式實例來看,首先是生產者:
Producer.java
package onlyfun.caterpillar;

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
package onlyfun.caterpillar;

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
package onlyfun.caterpillar;

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);
    ... // 執行一些動作進行判斷
}


使用這麼一個程式來測試:
WaitNotifyDemo.java
package onlyfun.caterpillar;

public class WaitNotifyDemo {
    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();
    }
}


執行結果:
生產者開始生產整數......
消費者開始消耗整數......
生產者設定 (1)
消費者取走 (1)
生產者設定 (2)
消費者取走 (2)
生產者設定 (3)
消費者取走 (3)
生產者設定 (4)
消費者取走 (4)
生產者設定 (5)
消費者取走 (5)
生產者設定 (6)
消費者取走 (6)
生產者設定 (7)
消費者取走 (7)
生產者設定 (8)
消費者取走 (8)
生產者設定 (9)
消費者取走 (9)
生產者設定 (10)
消費者取走 (10)


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

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

沒有留言:

張貼留言

你好!歡迎你在我的 Blog 上留下你寶貴的意見。