1. 程式人生 > >wait、notify、notifyAll的深入理解

wait、notify、notifyAll的深入理解

Java多執行緒的wait和notify/notifyAll方法是成對出現和使用的,要執行這兩個方法,有一個前提就是,當前執行緒必須獲取其物件的鎖,否則會丟擲IllegalMonitorStateException,所以這兩個方法必須在同步塊程式碼中呼叫。

生產者消費者模型是學習多執行緒知識中一個經典案例,一個典型的生產者消費者模型如下:
public void produce() {
        synchronized (this) {
            while (mBuf.isFull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();
        }
    }

    public void consume() {
        synchronized (this) {
            while (mBuf.isEmpty()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll();
        }
    }
我們可以從這段程式碼中引申出兩個問題:
1.為什麼wait方法外面是while迴圈而不是if判斷;
2.結尾處為什麼要用notifyAll方法,而不是notify方法。

要回答這兩個問題,首先要明白Java中物件鎖的模型。JVM會為一個使用內部鎖(synchronized)的物件維護兩個集合,Entry Set和Wait Set,也有人翻譯為鎖池和等待池。
對於Entry Set,如果執行緒A已經持有了物件鎖,此時如果有其它執行緒也想獲得該物件鎖的話,它只能進入Entry Set,並且處於執行緒的BLOCKED狀態;
對於Wait Set,如果執行緒A呼叫了wait方法,那麼執行緒A會釋放該物件的鎖,進入到Wait Set,並且處於執行緒的WAITING狀態。
還需要注意的是,某個執行緒B想要獲得物件鎖,一般情況下有兩個先決條件,一是物件鎖已經被釋放了(如曾經持有鎖的前任執行緒A執行完了synchronized程式碼塊或者呼叫了wait方法等等),二是執行緒B已處於RUNNABLE狀態。
那麼這兩類集合中的執行緒都是在什麼條件下可以轉變為RUNNABLE呢?
對於Entry Set中的執行緒,當物件鎖被釋放的時候,JVM會喚醒處於Entry Set中的某一個執行緒,這個執行緒的狀態就會從BLOCKED轉變為RUNNABLE;
對於Wait Set中的執行緒,當物件的notify方法被呼叫時,JVM會喚醒處於Wait Set中的某一個執行緒,這個執行緒的狀態就從WAITING轉變為RUNNABLE;或者當notifyAll方法被呼叫時,Wait Set中的全部執行緒會轉變為RUNNABLE狀態。所有Wait Set中被喚醒的執行緒會被轉移到Entry Set中。

然後,每當物件的鎖被釋放後,那些所有處於RUNNABLE狀態的執行緒會共同去競爭獲取物件的鎖,最終會有一個執行緒得勝,而其它競爭失敗的執行緒繼續在Entry Set中等待下一次機會。

有了這些知識點作為基礎,上述兩個問題就可以解釋清了。

首先看第一個問題。我們在呼叫wait方法的時候,心裡想的肯定是因為當前方法不滿足我們指定的條件,因此執行這個方法的執行緒需要等待直到其它執行緒改變了這個條件並且做出了通知。那麼為什麼要把wait方法放在迴圈而不是if裡呢?其實答案顯而易見,因為wait的執行緒永遠不能確定其它執行緒會在什麼狀態下notify,所以必須在被喚醒、搶佔到鎖並且從wait方法退出的時候再次進行指定條件的判斷,以決定是滿足條件往下執行還是不滿足條件再次wait。
就像這種本例中,如果只有一個生產者執行緒和一個消費者執行緒,使用if代替while並沒有問題,因為執行緒排程的行為是開發者可以預測的,生產者執行緒只有可能被消費者執行緒喚醒,反之亦然,程式不會出錯,只不過這種情況極為簡單,不夠普遍。

再看第二個問題。對於兩個生產者兩個消費者的場景,如果在程式碼中使用了notify而不是notifyAll,假設消費者執行緒1拿到了鎖,判斷buffer為空,那麼wait,釋放鎖;然後消費者2拿到鎖,同樣buffer為空,wait,此時Wait Set中有兩個執行緒;然後生產者1拿到鎖,生產,buffer滿,notify;生產者2拿到鎖,此時buffer是滿的,wait;消費者1拿到鎖,消費,notify;此時就有問題了,此時生產者2與消費者2都在Wait Set中,buffer為空,如果喚醒生產者2,沒毛病,但如果喚醒了消費者2,因為buffer為空,它會再次wait,萬一生產者1已經退出不再生產,此時只有生產者2與消費者2在Wait Set中相互等待,死鎖發生了。
但如果把上述例子中的notify換成notifyAll,這種情況就不會發生,因為每次notifyAll都會使其它等待的執行緒從Wait Set進入Entry Set,從而有機會獲得鎖。總之,儘量使用notifyAll的原因就是,notify非常容易造成死鎖。當然notifyAll並不是全是優點,畢竟一次性將Wait Set中的執行緒都喚醒也是一筆不菲的開銷。

完整的測試程式碼如下:
public class Test {
    
    private Buffer mBuf = new Buffer();

    public void produce() {
        synchronized (this) {
            while (mBuf.isFull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();
        }
    }

    public void consume() {
        synchronized (this) {
            while (mBuf.isEmpty()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll();
        }
    }

    private class Buffer {
        private static final int MAX_CAPACITY = 1;
        private List innerList = new ArrayList<Object>(MAX_CAPACITY);

        void add() {
            if (isFull()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.add(new Object());
            }
            System.out.println(Thread.currentThread().toString() + " add");

        }

        void remove() {
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.remove(MAX_CAPACITY - 1);
            }
            System.out.println(Thread.currentThread().toString() + " remove");
        }

        boolean isEmpty() {
            return innerList.isEmpty();
        }

        boolean isFull() {
            return innerList.size() == MAX_CAPACITY;
        }
    }

    public static void main(String[] args) {
        final Test sth = new Test();
        Runnable runProduce = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.produce();
                }
            }
        };
        Runnable runConsume = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.consume();
                }
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runConsume).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(runProduce).start();
        }
    }
}