你真的懂wait、notify和notifyAll嗎

生產者消費者模型是我們學習多線程知識的一個經典案例,一個典型的生產者消費者模型如下:

 

    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();
        }

    }

這段代碼很容易引申出來兩個問題:一個是wait()方法外面爲什麼是while循環而不是if判斷,另一個是結尾處的爲什麼要用notifyAll()方法,用notify()行嗎。

很多人在回答第二個問題的時候會想當然的說notify()是喚醒一個線程,notifyAll()是喚醒全部線程,但是喚醒然後呢,不管是notify()還是notifyAll(),最終拿到鎖的只會有一個線程,那它們到底有什麼區別呢?

其實這是一個對象內部鎖的調度問題,要回答這兩個問題,首先我們要明白java中對象鎖的模型,JVM會爲一個使用內部鎖(synchronized)的對象維護兩個集合,Entry SetWait 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狀態的線程會共同去競爭獲取對象的鎖,最終會有一個線程(具體哪一個取決於JVM實現,隊列裏的第一個?隨機的一個?)真正獲取到對象的鎖,而其他競爭失敗的線程繼續在Entry Set中等待下一次機會。

有了這些知識點作爲基礎,上述的兩個問題就能解釋的清了。

首先來看第一個問題,我們在調用wait()方法的時候,心裏想的肯定是因爲當前方法不滿足我們指定的條件,因此執行這個方法的線程需要等待直到其他線程改變了這個條件並且做出了通知。那麼爲什麼要把wait()方法放在循環而不是if判斷裏呢,其實答案顯而易見,因爲wait()的線程永遠不能確定其他線程會在什麼狀態下notify(),所以必須在被喚醒、搶佔到鎖並且從wait()方法退出的時候再次進行指定條件的判斷,以決定是滿足條件往下執行呢還是不滿足條件再次wait()呢。

就像在本例中,如果只有一個生產者線程,一個消費者線程,那其實是可以用if代替while的,因爲線程調度的行爲是開發者可以預測的,生產者線程只有可能被消費者線程喚醒,反之亦然,因此被喚醒時條件始終滿足,程序不會出錯。但是這種情況只是多線程情況下極爲簡單的一種,更普遍的是多個線程生產,多個線程消費,那麼就極有可能出現喚醒生產者的是另一個生產者或者喚醒消費者的是另一個消費者,這樣的情況下用if就必然會現類似過度生產或者過度消費的情況了,典型如IndexOutOfBoundsException的異常。所以所有的java書籍都會建議開發者永遠都要把wait()放到循環語句裏面

然後來看第二個問題,既然notify()和notifyAll()最終的結果都是隻有一個線程能拿到鎖,那喚醒一個和喚醒多個有什麼區別呢?

耐心看下面這個兩個生產者兩個消費者的場景,如果我們代碼中使用了notify()而非notifyAll(),假設消費者線程1拿到了鎖,判斷buffer爲空,那麼wait(),釋放鎖;然後消費者2拿到了鎖,同樣buffer爲空,wait(),也就是說此時Wait Set中有兩個線程;然後生產者1拿到鎖,生產,buffer滿,notify()了,那麼可能消費者1被喚醒了,但是此時還有另一個線程生產者2在Entry Set中盼望着鎖,並且最終搶佔到了鎖,但因爲此時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中的線程都喚醒是一筆不菲的開銷,如果你能handle你的線程調度,那麼使用notify()也是有好處的。

最後我把完整的測試代碼放出來,供大家參考:

 

import java.util.ArrayList;
import java.util.List;

public class Something {
    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<>(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) {
        Something sth = new Something();
        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();
        }
    }
}
  • 上面的栗子是正確的使用方式,輸出的結果如下:

 

Thread[Thread-2,5,main] add
Thread[Thread-1,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-0,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-0,5,main] remove
Thread[Thread-2,5,main] add
Thread[Thread-1,5,main] remove

Process finished with exit code 0
  • 如果把while改成if,結果如下,程序可能產生運行時異常:

 

Thread[Thread-2,5,main] add
Thread[Thread-1,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-1,5,main] remove
Thread[Thread-3,5,main] add
Thread[Thread-1,5,main] remove
Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.IndexOutOfBoundsException
    at Something$Buffer.add(Something.java:42)
    at Something.produce(Something.java:16)
    at Something$1.run(Something.java:76)
    at java.lang.Thread.run(Thread.java:748)
java.lang.IndexOutOfBoundsException
    at Something$Buffer.remove(Something.java:52)
    at Something.consume(Something.java:30)
    at Something$2.run(Something.java:86)
    at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0
  • 如果把notifyAll改爲notify,結果如下,死鎖,程序沒有正常退出:

 

Thread[Thread-2,5,main] add
Thread[Thread-0,5,main] remove
Thread[Thread-3,5,main] add



作者:A_客
鏈接:https://www.jianshu.com/p/25e243850bd2
 

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