搞定等待通知機(jī)制-wait/notify/notifyall的2個(gè)經(jīng)典面試題(實(shí)例詳解)

前言

關(guān)于wait/notify/notifyall有2個(gè)經(jīng)典的面試:

  • notify和notifyall有什么區(qū)別在抛?
  • 為什么wait方法要寫(xiě)在while循環(huán)里面而不是if呢?

帶著這2個(gè)問(wèn)題娇唯,我們來(lái)學(xué)習(xí)下synchronized提供的等待通知機(jī)制。



1拂铡、你要知道的基本知識(shí)

  • wait/notify/notifyall都是存在與Object類里面的方法熏瞄。

ps:為什么要存在Object里面而不是Thread里面呢?趕緊看下這篇吧(<u>大徹大悟synchronized原理也搓,鎖的升級(jí)</u>)赏廓,從底層了解其原因。

  • wait方法:wait方法只是無(wú)參和帶超時(shí)時(shí)間2種方法傍妒,調(diào)用wait方法的的線程會(huì)進(jìn)入waiting或timed_waiting狀態(tài)幔摸;
  • notify方法:調(diào)用notify方法的線程,會(huì)喚醒一個(gè)處于waiting狀態(tài)的線程颤练;
  • notifyall方法:調(diào)用notify方法的線程既忆,會(huì)喚醒所有處于waiting狀態(tài)的線程;

有一個(gè)前提嗦玖,就是wait/notify/notifyall方法必須在獲取到synchronized資源鎖的情況下患雇,才能調(diào)用,也就是wait/notify/notifyall必須在synchronized代碼塊里面宇挫。

使用wait/notify/notifyal有個(gè)經(jīng)典的范式苛吱,這便是上面的第二個(gè)問(wèn)題。

  while(條件不滿足) {
    wait();
  }



2器瘪、典型的生產(chǎn)者消費(fèi)者模式的樣例

現(xiàn)在我們寫(xiě)個(gè)典型的生產(chǎn)者消費(fèi)者模式的樣例:

  • 有2個(gè)生產(chǎn)者翠储,當(dāng)list不滿的情況下绘雁,往list里面添加數(shù)據(jù),當(dāng)list滿的情況下調(diào)用wait方法援所。每添加一條數(shù)據(jù)庐舟,就調(diào)用notifyAll()方法。
  • 2個(gè)消費(fèi)者住拭,當(dāng)list不為空的情況下挪略,消費(fèi)list里面的第一個(gè)元素,并且調(diào)用notifyAll()方法滔岳。
  • 為了效果杠娱,我讓每個(gè)生產(chǎn)者只生產(chǎn)了3條數(shù)據(jù),消費(fèi)者一直在消費(fèi)澈蟆,最終都進(jìn)入waiting狀態(tài)墨辛。
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class WaitNotifyTest {
    private List<String> list = new ArrayList<>();

    public static void main(String[] args) {
        WaitNotifyTest waitNotifyTest = new WaitNotifyTest();
        Thread producer1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                waitNotifyTest.produce();
            }
        });
        producer1.setName("生產(chǎn)者1號(hào)");
        Thread producer2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                waitNotifyTest.produce();
            }
        });
        producer2.setName("生產(chǎn)者2號(hào)");

        Thread consumer1 = new Thread(() -> {
            while (true) waitNotifyTest.consume();
        });
        consumer1.setName("消費(fèi)者1號(hào)");
        Thread consumer2 = new Thread(() -> {
            while (true) waitNotifyTest.consume();
        });
        consumer2.setName("消費(fèi)者2號(hào)");

        producer1.start();
        producer2.start();
        consumer1.start();
        consumer2.start();
    }

    private void produce() {
        synchronized (this) {
            while (listIsFull()) {
                try {
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入等待池");
                    wait();
                    System.out.println(Thread.currentThread().getName() + "被喚醒了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            String value = UUID.randomUUID().toString();
            System.out.println(Thread.currentThread().getName() + "生產(chǎn)了一條消息:" + value);
            list.add(value);
            notifyAll();
        }
    }

    private void consume() {
        synchronized (this) {
            while (listIsEmpty()) {
                try {
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入等待池");
                    wait();
                    System.out.println(Thread.currentThread().getName() + "被喚醒了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "消費(fèi)了一條消息:" + list.get(0));
            list.remove(0);
            notifyAll();
        }
    }

    private boolean listIsFull() {
        return list.size() == 1;
    }

    private boolean listIsEmpty() {
        return list.size() == 0;
    }
}

執(zhí)行結(jié)果:

生產(chǎn)者1號(hào)生產(chǎn)了一條消息:85158920-3784-4c5f-9c2a-2961aea75086
消費(fèi)者2號(hào)消費(fèi)了一條消息:85158920-3784-4c5f-9c2a-2961aea75086
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者2號(hào)生產(chǎn)了一條消息:3caf8352-28d3-44f6-8a7f-5ecf7356e903
生產(chǎn)者2號(hào)進(jìn)入等待池
消費(fèi)者1號(hào)消費(fèi)了一條消息:3caf8352-28d3-44f6-8a7f-5ecf7356e903
消費(fèi)者1號(hào)進(jìn)入等待池
生產(chǎn)者2號(hào)被喚醒了
生產(chǎn)者2號(hào)生產(chǎn)了一條消息:b8764c83-8b42-4527-a953-4424e1103111
生產(chǎn)者2號(hào)進(jìn)入等待池
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)消費(fèi)了一條消息:b8764c83-8b42-4527-a953-4424e1103111
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者1號(hào)生產(chǎn)了一條消息:32228e02-d7f1-4866-83c7-d3fcfb6a51b4
生產(chǎn)者1號(hào)進(jìn)入等待池
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)消費(fèi)了一條消息:32228e02-d7f1-4866-83c7-d3fcfb6a51b4
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者2號(hào)被喚醒了
生產(chǎn)者2號(hào)生產(chǎn)了一條消息:3af2d730-7bf3-4a26-8a96-f6f3f30fc39a
消費(fèi)者1號(hào)被喚醒了
消費(fèi)者1號(hào)消費(fèi)了一條消息:3af2d730-7bf3-4a26-8a96-f6f3f30fc39a
消費(fèi)者1號(hào)進(jìn)入等待池
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者1號(hào)被喚醒了
生產(chǎn)者1號(hào)生產(chǎn)了一條消息:0867b9ae-de1a-486b-b0b0-e56c6b10a49b
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)消費(fèi)了一條消息:0867b9ae-de1a-486b-b0b0-e56c6b10a49b
消費(fèi)者2號(hào)進(jìn)入等待池
消費(fèi)者1號(hào)被喚醒了
消費(fèi)者1號(hào)進(jìn)入等待池



3、notify和notifyAll有什么區(qū)別

最簡(jiǎn)單的回答就是:notify只喚醒一個(gè)waiting狀態(tài)的線程趴俘,notifyAll喚醒所有waiting狀態(tài)的線程睹簇。

但是要徹底弄清楚它們的區(qū)別,還是要從synchronized的底層說(shuō)起寥闪√荩看過(guò)這篇文章的顯然已經(jīng)知道答案了。
[圖片上傳失敗...(image-2d12eb-1605322860624)]

我這里再整理下疲憋。

  • synchronized維護(hù)的對(duì)象鎖有2個(gè)隊(duì)列凿渊,一個(gè)_EntryList,一個(gè)_WaitSet缚柳。
  • 加鎖時(shí)埃脏,線程獲取到鎖進(jìn)入臨界區(qū)(_owner),若線程獲取不到鎖秋忙,便加入_EntryList彩掐,進(jìn)入blocked阻塞狀態(tài)。
  • 線程獲取到鎖后灰追,調(diào)用wait方法堵幽,被加入_WaitSet隊(duì)列,進(jìn)入waiting狀態(tài)弹澎,然后等待喚醒朴下。當(dāng)線程被喚醒的時(shí)候,被喚醒的線程需要再次獲取對(duì)象鎖苦蒿。
  • 喚醒線程殴胧,我們可以調(diào)用notify和notifyAll方法,notify只是隨機(jī)的喚醒一個(gè)_WaitSet中的線程佩迟。notifyAll會(huì)喚醒所有處于_WaitSet中的線程溃肪。
  • 不管喚醒一個(gè)線程免胃,還是喚醒多個(gè)線程,最終獲得對(duì)象鎖的惫撰,只有一個(gè)線程。如果_EntryList同時(shí)存在競(jìng)爭(zhēng)鎖資源的線程躺涝,那么被喚醒的線程還需要和_EntryList中的線程一起競(jìng)爭(zhēng)鎖資源厨钻。但是JVM保證最終只會(huì)讓一個(gè)線程獲取到鎖

那如果只喚醒一個(gè)線程會(huì)有什么問(wèn)題呢坚嗜?
拿上面的生產(chǎn)者消費(fèi)者舉個(gè)例子:

  • 當(dāng)list為空時(shí)夯膀,消費(fèi)者consumer1、consumer2都會(huì)處于waiting狀態(tài)
  • 生產(chǎn)者producer1和生產(chǎn)者producer2競(jìng)爭(zhēng)鎖苍蔬,生產(chǎn)者producer1先拿到鎖诱建,生產(chǎn)一條數(shù)據(jù),調(diào)用notify(假設(shè)喚醒的是消費(fèi)者consumer1)碟绑,然后producer1進(jìn)入阻塞blocked狀態(tài)俺猿,并釋放鎖;
  • 消費(fèi)者consumer1被喚醒后格仲,有三個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖(producer1押袍、producer2、consumer1)凯肋,假設(shè)producer2獲得鎖谊惭,producer2發(fā)現(xiàn)list滿了,然后進(jìn)入waiting狀態(tài)侮东,并釋放鎖圈盔;
  • 鎖被釋放后,有兩個(gè)個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖(producer1悄雅、consumer1)驱敲,假設(shè)consumer1獲取到鎖,consumer1消費(fèi)消息煤伟,然后調(diào)用notify(此時(shí)consumer2和生產(chǎn)者producer2處于waiting)癌佩;
    問(wèn)題就出在這里:
    假設(shè)喚醒的是生產(chǎn)者producer2,沒(méi)有問(wèn)題便锨;
    假設(shè)喚醒的是消費(fèi)者consumer2围辙,那consumer2會(huì)發(fā)現(xiàn)list任然為空,繼續(xù)進(jìn)入waiting狀態(tài)放案。但是呢姚建,恰好之前進(jìn)入阻塞狀態(tài)的producer1已經(jīng)下線(在這個(gè)例子中就是生產(chǎn)了3條數(shù)據(jù))。這樣就出現(xiàn)了死鎖了(producer2和consumer2都處于waiting狀態(tài)吱殉,消費(fèi)者consumer1在獲取到鎖后也會(huì)進(jìn)入wait狀態(tài))

所以說(shuō)掸冤,使用notify某些希望被喚醒的線程厘托,永遠(yuǎn)得不到喚醒,獲取不到鎖資源稿湿,導(dǎo)致死鎖铅匹。

綜上所述:我們盡量使用notifyAll而不是notify。除非你經(jīng)過(guò)深思熟慮饺藤,且明確知道喚醒的就你希望的線程(比如上例中只有一個(gè)生產(chǎn)者一個(gè)消費(fèi)者)



4包斑、為什么wait方法要寫(xiě)在while循環(huán)里面而不是if呢

再來(lái)看下第二個(gè)問(wèn)題:為什么wait方法要寫(xiě)在while循環(huán)里面而不是if呢?

private void produce() {
    synchronized (this) {
        while (listIsFull()) {
            try {
                System.out.println(Thread.currentThread().getName() + "進(jìn)入等待池");
                wait();
                System.out.println(Thread.currentThread().getName() + "被喚醒了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String value = UUID.randomUUID().toString();
        System.out.println(Thread.currentThread().getName() + "生產(chǎn)了一條消息:" + value);
        list.add(value);
        notifyAll();
    }
}

明確這2點(diǎn):

  • 線程被喚醒之后涕俗,代碼是緊接著執(zhí)行wait后面的代碼(從上面的執(zhí)行結(jié)果可以看出)罗丰;
  • 進(jìn)入waiting狀態(tài)的線程被喚醒的條件是“條件滿足”,對(duì)應(yīng)到下面的例子就是隊(duì)列不滿再姑。
    被喚醒的線程需要和其他線程競(jìng)爭(zhēng)鎖資源(最終只有一個(gè)線程獲取到)萌抵,那么當(dāng)被喚醒的線程獲得鎖資源的時(shí)候,之前的條件可能又不滿足了元镀。
    如果while改成if绍填,那么被喚醒的線程繼續(xù)執(zhí)行(默認(rèn)條件任然滿足),這明顯會(huì)導(dǎo)致并發(fā)問(wèn)題凹联,比如超額生產(chǎn)沐兰、消費(fèi)。



5蔽挠、總結(jié)

以后遇到同樣的問(wèn)題知道怎么回答了吧住闯!


ps:一天一個(gè)IDEA小技巧
快捷鍵[Alt+7]可以打開(kāi)當(dāng)前類的架構(gòu)圖(Structure),可以快速查看類澳淑、方法比原、字段等。這樣可以提升工作效率哦杠巡,不要用鼠標(biāo)滾動(dòng)查找啦A烤健!氢拥!

例:


image.png

多線程連載:
<u>Java內(nèi)存模型-volatile的應(yīng)用(實(shí)例講解)</u>
<u>synchronized的三種應(yīng)用方式(實(shí)例講解)</u>
<u>可重入鎖-synchronized是可重入鎖嗎蚌铜?</u>
<u>大徹大悟synchronized原理,鎖的升級(jí)</u>
<u>一文弄懂Java的線程池</u>
<u>公平鎖和非公平鎖-ReentrantLock是如何實(shí)現(xiàn)公平嫩海、非公平的</u>
<u>一圖全面了解Java線程的生命周期</u>
<u>守護(hù)線程和用戶線程的真正區(qū)別(實(shí)例講解)</u>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冬殃,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子叁怪,更是在濱河造成了極大的恐慌审葬,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異涣觉,居然都是意外死亡痴荐,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門官册,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)生兆,“玉大人,你說(shuō)我怎么就攤上這事膝宁≡矸罚” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵昆汹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我婴栽,道長(zhǎng)满粗,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任愚争,我火速辦了婚禮映皆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘轰枝。我一直安慰自己捅彻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鞍陨。 她就那樣靜靜地躺著步淹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诚撵。 梳的紋絲不亂的頭發(fā)上缭裆,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音寿烟,去河邊找鬼澈驼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛筛武,可吹牛的內(nèi)容都是我干的缝其。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼徘六,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼内边!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起硕噩,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤假残,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體辉懒,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阳惹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眶俩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莹汤。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖颠印,靈堂內(nèi)的尸體忽然破棺而出纲岭,到底是詐尸還是另有隱情,我是刑警寧澤线罕,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布止潮,位于F島的核電站,受9級(jí)特大地震影響钞楼,放射性物質(zhì)發(fā)生泄漏喇闸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一询件、第九天 我趴在偏房一處隱蔽的房頂上張望燃乍。 院中可真熱鬧,春花似錦宛琅、人聲如沸刻蟹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)舆瘪。三九已至,卻和暖如春仓洼,著一層夾襖步出監(jiān)牢的瞬間介陶,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工色建, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哺呜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓箕戳,卻偏偏與公主長(zhǎng)得像某残,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子陵吸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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