你真的懂wait、notify和notifyAll嗎

生產(chǎn)者消費(fèi)者模型是我們學(xué)習(xí)多線程知識的一個經(jīng)典案例,一個典型的生產(chǎn)者消費(fèi)者模型如下:

    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循環(huán)而不是if判斷,另一個是結(jié)尾處的為什么要用notifyAll()方法,用notify()行嗎。

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

其實(shí)這是一個對象內(nèi)部鎖的調(diào)度問題,要回答這兩個問題,首先我們要明白java中對象鎖的模型,JVM會為一個使用內(nèi)部鎖(synchronized)的對象維護(hù)兩個集合,Entry SetWait Set,也有人翻譯為鎖池和等待池,意思基本一致。

對于Entry Set:如果線程A已經(jīng)持有了對象鎖,此時如果有其他線程也想獲得該對象鎖的話,它只能進(jìn)入Entry Set,并且處于線程的BLOCKED狀態(tài)。

對于Wait Set:如果線程A調(diào)用了wait()方法,那么線程A會釋放該對象的鎖,進(jìn)入到Wait Set,并且處于線程的WAITING狀態(tài)。

還有需要注意的是,某個線程B想要獲得對象鎖,一般情況下有兩個先決條件,一是對象鎖已經(jīng)被釋放了(如曾經(jīng)持有鎖的前任線程A執(zhí)行完了synchronized代碼塊或者調(diào)用了wait()方法等等),二是線程B已處于RUNNABLE狀態(tài)。

那么這兩類集合中的線程都是在什么條件下可以轉(zhuǎn)變?yōu)镽UNNABLE呢?

對于Entry Set中的線程,當(dāng)對象鎖被釋放的時候,JVM會喚醒處于Entry Set中的某一個線程,這個線程的狀態(tài)就從BLOCKED轉(zhuǎn)變?yōu)镽UNNABLE。

對于Wait Set中的線程,當(dāng)對象的notify()方法被調(diào)用時,JVM會喚醒處于Wait Set中的某一個線程,這個線程的狀態(tài)就從WAITING轉(zhuǎn)變?yōu)镽UNNABLE;或者當(dāng)notifyAll()方法被調(diào)用時,Wait Set中的全部線程會轉(zhuǎn)變?yōu)镽UNNABLE狀態(tài)。所有Wait Set中被喚醒的線程會被轉(zhuǎn)移到Entry Set中。

然后,每當(dāng)對象的鎖被釋放后,那些所有處于RUNNABLE狀態(tài)的線程會共同去競爭獲取對象的鎖,最終會有一個線程(具體哪一個取決于JVM實(shí)現(xiàn),隊(duì)列里的第一個?隨機(jī)的一個?)真正獲取到對象的鎖,而其他競爭失敗的線程繼續(xù)在Entry Set中等待下一次機(jī)會。

有了這些知識點(diǎn)作為基礎(chǔ),上述的兩個問題就能解釋的清了。

首先來看第一個問題,我們在調(diào)用wait()方法的時候,心里想的肯定是因?yàn)楫?dāng)前方法不滿足我們指定的條件,因此執(zhí)行這個方法的線程需要等待直到其他線程改變了這個條件并且做出了通知。那么為什么要把wait()方法放在循環(huán)而不是if判斷里呢,其實(shí)答案顯而易見,因?yàn)閣ait()的線程永遠(yuǎn)不能確定其他線程會在什么狀態(tài)下notify(),所以必須在被喚醒、搶占到鎖并且從wait()方法退出的時候再次進(jìn)行指定條件的判斷,以決定是滿足條件往下執(zhí)行呢還是不滿足條件再次wait()呢。

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

然后來看第二個問題,既然notify()和notifyAll()最終的結(jié)果都是只有一個線程能拿到鎖,那喚醒一個和喚醒多個有什么區(qū)別呢?

耐心看下面這個兩個生產(chǎn)者兩個消費(fèi)者的場景,如果我們代碼中使用了notify()而非notifyAll(),假設(shè)消費(fèi)者線程1拿到了鎖,判斷buffer為空,那么wait(),釋放鎖;然后消費(fèi)者2拿到了鎖,同樣buffer為空,wait(),也就是說此時Wait Set中有兩個線程;然后生產(chǎn)者1拿到鎖,生產(chǎn),buffer滿,notify()了,那么可能消費(fèi)者1被喚醒了,但是此時還有另一個線程生產(chǎn)者2在Entry Set中盼望著鎖,并且最終搶占到了鎖,但因?yàn)榇藭rbuffer是滿的,因此它要wait();然后消費(fèi)者1拿到了鎖,消費(fèi),notify();這時就有問題了,此時生產(chǎn)者2和消費(fèi)者2都在Wait Set中,buffer為空,如果喚醒生產(chǎn)者2,沒毛病;但如果喚醒了消費(fèi)者2,因?yàn)閎uffer為空,它會再次wait(),這就尷尬了,萬一生產(chǎn)者1已經(jīng)退出不再生產(chǎn)了,沒有其他線程在競爭鎖了,只有生產(chǎn)者2和消費(fèi)者2在Wait Set中互相等待,那傳說中的死鎖就發(fā)生了。

但如果你把上述例子中的notify()換成notifyAll(),這樣的情況就不會再出現(xiàn)了,因?yàn)槊看蝞otifyAll()都會使其他等待的線程從Wait Set進(jìn)入Entry Set,從而有機(jī)會獲得鎖。

其實(shí)說了這么多,一句話解釋就是之所以我們應(yīng)該盡量使用notifyAll()的原因就是,notify()非常容易導(dǎo)致死鎖。當(dāng)然notifyAll并不一定都是優(yōu)點(diǎn),畢竟一次性將Wait Set中的線程都喚醒是一筆不菲的開銷,如果你能handle你的線程調(diào)度,那么使用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();
        }
    }
}
  • 上面的栗子是正確的使用方式,輸出的結(jié)果如下:
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,結(jié)果如下,程序可能產(chǎn)生運(yùn)行時異常:
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,結(jié)果如下,死鎖,程序沒有正常退出:
Thread[Thread-2,5,main] add
Thread[Thread-0,5,main] remove
Thread[Thread-3,5,main] add

另,個人技術(shù)博客,同步更新,歡迎關(guān)注!轉(zhuǎn)載請注明出處!文中若有什么錯誤希望大家探討指正!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 本文出自 Eddy Wiki ,轉(zhuǎn)載請注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 2,232評論 0 14
  • Java SE 基礎(chǔ): 封裝、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨(dú)立的整體,并盡...
    Jayden_Cao閱讀 2,134評論 0 8
  • 簡書 賈小強(qiáng)轉(zhuǎn)載請注明原創(chuàng)出處,謝謝! Java多線程是個很復(fù)雜的問題,尤其在多線程在任何給定的時間訪問共享資源需...
    賈小強(qiáng)閱讀 1,538評論 0 1
  • 一、進(jìn)程和線程 進(jìn)程 進(jìn)程就是一個執(zhí)行中的程序?qū)嵗總€進(jìn)程都有自己獨(dú)立的一塊內(nèi)存空間,一個進(jìn)程中可以有多個線程。...
    阿敏其人閱讀 2,622評論 0 13
  • 1 朋友L姑娘在群里激動了一晚上,并不是因?yàn)樽蛱焓桥窆?jié),也不是因?yàn)榻裉焓菋D女節(jié)。很簡單,只是因?yàn)樗猩裢蝗徽宜?..
    范文盲閱讀 431評論 1 2