<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          求求你,別再用wait和notify了!

          共 10829字,需瀏覽 22分鐘

           ·

          2020-12-20 12:13

          作者 | 王磊

          來源 | Java中文社群(ID:javacn666)

          轉(zhuǎn)載請(qǐng)聯(lián)系授權(quán)(微信ID:GG_Stone)

          Condition 是 JDK 1.5 中提供的用來替代 waitnotify 的線程通訊方法,那么一定會(huì)有人問:為什么不能用 waitnotify 了? 哥們我用的好好的。老弟別著急,聽我給你細(xì)說...

          之所以推薦使用 Condition 而非 Object 中的 waitnotify?的原因有兩個(gè):

          1. 使用 notify 在極端環(huán)境下會(huì)造成線程“假死”;
          2. Condition 性能更高。

          接下來怎們就用代碼和流程圖的方式來演示上述的兩種情況。

          1.notify 線程“假死”

          所謂的線程“假死”是指,在使用 notify 喚醒多個(gè)等待的線程時(shí),卻意外的喚醒了一個(gè)沒有“準(zhǔn)備好”的線程,從而導(dǎo)致整個(gè)程序進(jìn)入了阻塞的狀態(tài)不能繼續(xù)執(zhí)行。

          以多線程編程中的經(jīng)典案例生產(chǎn)者和消費(fèi)者模型為例,我們先來演示一下線程“假死”的問題。

          1.1 正常版本

          在演示線程“假死”的問題之前,我們先使用 wait?和 notify?來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的生產(chǎn)者和消費(fèi)者模型,為了讓代碼更直觀,我這里寫一個(gè)超級(jí)簡(jiǎn)單的實(shí)現(xiàn)版本。我們先來創(chuàng)建一個(gè)工廠類,工廠類里面包含兩個(gè)方法,一個(gè)是循環(huán)生產(chǎn)數(shù)據(jù)的(存入)方法,另一個(gè)是循環(huán)消費(fèi)數(shù)據(jù)的(取出)方法,實(shí)現(xiàn)代碼如下。

          /**
          ?*?工廠類,消費(fèi)者和生產(chǎn)者通過調(diào)用工廠類實(shí)現(xiàn)生產(chǎn)/消費(fèi)
          ?*/

          class?Factory?{
          ????private?int[]?items?=?new?int[1];?//?數(shù)據(jù)存儲(chǔ)容器(為了演示方便,設(shè)置容量最多存儲(chǔ)?1?個(gè)元素)
          ????private?int?size?=?0;?????????????//?實(shí)際存儲(chǔ)大小

          ????/**
          ?????*?生產(chǎn)方法
          ?????*/

          ????public?synchronized?void?put()?throws?InterruptedException?{
          ????????//?循環(huán)生產(chǎn)數(shù)據(jù)
          ????????do?{
          ????????????while?(size?==?items.length)?{?//?注意不能是?if?判斷
          ????????????????//?存儲(chǔ)的容量已經(jīng)滿了,阻塞等待消費(fèi)者消費(fèi)之后喚醒
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?進(jìn)入阻塞");
          ????????????????this.wait();
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?被喚醒");
          ????????????}
          ????????????System.out.println(Thread.currentThread().getName()?+?"?開始工作");
          ????????????items[0]?=?1;?//?為了方便演示,設(shè)置固定值
          ????????????size++;
          ????????????System.out.println(Thread.currentThread().getName()?+?"?完成工作");
          ????????????//?當(dāng)生產(chǎn)隊(duì)列有數(shù)據(jù)之后通知喚醒消費(fèi)者
          ????????????this.notify();

          ????????}?while?(true);
          ????}

          ????/**
          ?????*?消費(fèi)方法
          ?????*/

          ????public?synchronized?void?take()?throws?InterruptedException?{
          ????????//?循環(huán)消費(fèi)數(shù)據(jù)
          ????????do?{
          ????????????while?(size?==?0)?{
          ????????????????//?生產(chǎn)者沒有數(shù)據(jù),阻塞等待
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?進(jìn)入阻塞(消費(fèi)者)");
          ????????????????this.wait();
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?被喚醒(消費(fèi)者)");
          ????????????}
          ????????????System.out.println("消費(fèi)者工作~");
          ????????????size--;
          ????????????//?喚醒生產(chǎn)者可以添加生產(chǎn)了
          ????????????this.notify();
          ????????}?while?(true);
          ????}
          }

          接下來我們來創(chuàng)建兩個(gè)線程,一個(gè)是生產(chǎn)者調(diào)用 put?方法,另一個(gè)是消費(fèi)者調(diào)用 take?方法,實(shí)現(xiàn)代碼如下:

          public?class?NotifyDemo?{
          ????public?static?void?main(String[]?args)?{
          ????????//?創(chuàng)建工廠類
          ????????Factory?factory?=?new?Factory();

          ????????//?生產(chǎn)者
          ????????Thread?producer?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.put();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"生產(chǎn)者");
          ????????producer.start();

          ????????//?消費(fèi)者
          ????????Thread?consumer?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.take();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"消費(fèi)者");
          ????????consumer.start();
          ????}
          }

          執(zhí)行結(jié)果如下:從上述結(jié)果可以看出,生產(chǎn)者和消費(fèi)者在循環(huán)交替的執(zhí)行任務(wù),場(chǎng)面非常和諧,是我們想要的正確結(jié)果。

          1.2 線程“假死”版本

          當(dāng)只有一個(gè)生產(chǎn)者和一個(gè)消費(fèi)者時(shí),waitnotify?方法不會(huì)有任何問題,然而將生產(chǎn)者增加到兩個(gè)時(shí)就會(huì)出現(xiàn)線程“假死”的問題了,程序的實(shí)現(xiàn)代碼如下:

          public?class?NotifyDemo?{
          ????public?static?void?main(String[]?args)?{
          ??//?創(chuàng)建工廠方法(工廠類的代碼不變,這里不再?gòu)?fù)述)
          ????????Factory?factory?=?new?Factory();

          ????????//?生產(chǎn)者
          ????????Thread?producer?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.put();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"生產(chǎn)者");
          ????????producer.start();

          ????????//?生產(chǎn)者?2
          ????????Thread?producer2?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.put();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"生產(chǎn)者2");
          ????????producer2.start();
          ????????
          ????????//?消費(fèi)者
          ????????Thread?consumer?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.take();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"消費(fèi)者");
          ????????consumer.start();
          ????}
          }

          程序執(zhí)行結(jié)果如下:從以上結(jié)果可以看出,當(dāng)我們將生產(chǎn)者的數(shù)量增加到 2 個(gè)時(shí),就會(huì)造成線程“假死”阻塞執(zhí)行的問題,當(dāng)生產(chǎn)者 2 被喚醒又被阻塞之后,整個(gè)程序就不能繼續(xù)執(zhí)行了。

          線程“假死”問題分析

          我們先把以上程序的執(zhí)行步驟標(biāo)注一下,得到如下結(jié)果:從上圖可以看出:當(dāng)執(zhí)行到第 ④ 步時(shí),此時(shí)生產(chǎn)者為工作狀態(tài),而生產(chǎn)者 2 和消費(fèi)者為等待狀態(tài),此時(shí)正確的做法應(yīng)該是喚醒消費(fèi)著進(jìn)行消費(fèi),然后消費(fèi)者消費(fèi)完之后再喚醒生產(chǎn)者繼續(xù)工作;但此時(shí)生產(chǎn)者卻錯(cuò)誤的喚醒了生產(chǎn)者 2,而生產(chǎn)者 2 因?yàn)殛?duì)列已經(jīng)滿了,所以自身并不具備繼續(xù)執(zhí)行的能力,因此就導(dǎo)致了整個(gè)程序的阻塞,流程圖如下所示:

          正確執(zhí)行流程應(yīng)該是這樣的:

          1.3 使用 Condition

          為了解決線程的“假死”問題,我們可以使用 Condition?來嘗試實(shí)現(xiàn)一下,Condition 是 JUC(java.util.concurrent)包下的類,需要使用 Lock 鎖來創(chuàng)建,Condition 提供了 3 個(gè)重要的方法:

          • await:對(duì)應(yīng) wait?方法;
          • signal:對(duì)應(yīng) notify 方法;
          • signalAllnotifyAll?方法。

          Condition?的使用和 wait/notify?類似,也是先獲得鎖然后在鎖中進(jìn)行等待和喚醒操作,Condition?的基礎(chǔ)用法如下:

          //?創(chuàng)建?Condition?對(duì)象
          Lock?lock?=?new?ReentrantLock();
          Condition?condition?=?lock.newCondition();
          //?加鎖
          lock.lock();
          try?{
          ????//?業(yè)務(wù)方法....
          ????
          ????//?1.進(jìn)入等待狀態(tài)
          ????condition.await();

          ????//?2.喚醒操作
          ????condition.signal();
          }?catch?(InterruptedException?e)?{
          ????e.printStackTrace();
          }?finally?{
          ????lock.unlock();
          }

          小知識(shí):Lock的正確使用姿勢(shì)

          切記 Lock?的 lock.lock()?方法不能放入 try?代碼中,如果 lock 方法在 try 代碼塊之內(nèi),可能由于其它方法拋出異常,導(dǎo)致在 finally 代碼塊中, unlock 對(duì)未加鎖的對(duì)象解鎖,它會(huì)調(diào)用 AQStryRelease 方法(取決于具體實(shí)現(xiàn)類),拋出 IllegalMonitorStateException 異常。

          回歸主題

          回到本文的主題,我們?nèi)绻褂?Condition?來實(shí)現(xiàn)線程的通訊就可以避免程序的“假死”情況,因?yàn)?Condition?可以創(chuàng)建多個(gè)等待集,以本文的生產(chǎn)者和消費(fèi)者模型為例,我們可以使用兩個(gè)等待集,一個(gè)用做消費(fèi)者的等待和喚醒,另一個(gè)用來喚醒生產(chǎn)者,這樣就不會(huì)出現(xiàn)生產(chǎn)者喚醒生產(chǎn)者的情況了(生產(chǎn)者只能喚醒消費(fèi)者,消費(fèi)者只能喚醒生產(chǎn)者)這樣整個(gè)流程就不會(huì)“假死”了,它的執(zhí)行流程如下圖所示:了解了它的基本流程之后,咱們來看具體的實(shí)現(xiàn)代碼。

          基于 Condition?的工廠實(shí)現(xiàn)代碼如下:

          class?FactoryByCondition?{
          ????private?int[]?items?=?new?int[1];?//?數(shù)據(jù)存儲(chǔ)容器(為了演示方便,設(shè)置容量最多存儲(chǔ)?1?個(gè)元素)
          ????private?int?size?=?0;?????????????//?實(shí)際存儲(chǔ)大小
          ????//?創(chuàng)建?Condition?對(duì)象
          ????private?Lock?lock?=?new?ReentrantLock();
          ????//?生產(chǎn)者的?Condition?對(duì)象
          ????private?Condition?producerCondition?=?lock.newCondition();
          ????//?消費(fèi)者的?Condition?對(duì)象
          ????private?Condition?consumerCondition?=?lock.newCondition();

          ????/**
          ?????*?生產(chǎn)方法
          ?????*/

          ????public?void?put()?throws?InterruptedException?{
          ????????//?循環(huán)生產(chǎn)數(shù)據(jù)
          ????????do?{
          ????????????lock.lock();
          ????????????while?(size?==?items.length)?{?//?注意不能是?if?判斷
          ????????????????//?生產(chǎn)者進(jìn)入等待
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?進(jìn)入阻塞");
          ????????????????producerCondition.await();
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?被喚醒");
          ????????????}
          ????????????System.out.println(Thread.currentThread().getName()?+?"?開始工作");
          ????????????items[0]?=?1;?//?為了方便演示,設(shè)置固定值
          ????????????size++;
          ????????????System.out.println(Thread.currentThread().getName()?+?"?完成工作");
          ????????????//?喚醒消費(fèi)者
          ????????????consumerCondition.signal();
          ????????????try?{
          ????????????}?finally?{
          ????????????????lock.unlock();
          ????????????}
          ????????}?while?(true);
          ????}

          ????/**
          ?????*?消費(fèi)方法
          ?????*/

          ????public?void?take()?throws?InterruptedException?{
          ????????//?循環(huán)消費(fèi)數(shù)據(jù)
          ????????do?{
          ????????????lock.lock();
          ????????????while?(size?==?0)?{
          ????????????????//?消費(fèi)者阻塞等待
          ????????????????consumerCondition.await();
          ????????????}
          ????????????System.out.println("消費(fèi)者工作~");
          ????????????size--;
          ????????????//?喚醒生產(chǎn)者
          ????????????producerCondition.signal();
          ????????????try?{
          ????????????}?finally?{
          ????????????????lock.unlock();
          ????????????}
          ????????}?while?(true);
          ????}
          }

          兩個(gè)生產(chǎn)者和一個(gè)消費(fèi)者的實(shí)現(xiàn)代碼如下:

          public?class?NotifyDemo?{
          ????public?static?void?main(String[]?args)?{
          ????????FactoryByCondition?factory?=?new?FactoryByCondition();

          ????????//?生產(chǎn)者
          ????????Thread?producer?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.put();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"生產(chǎn)者");
          ????????producer.start();

          ????????//?生產(chǎn)者?2
          ????????Thread?producer2?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.put();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"生產(chǎn)者2");
          ????????producer2.start();

          ????????//?消費(fèi)者
          ????????Thread?consumer?=?new?Thread(()?->?{
          ????????????try?{
          ????????????????factory.take();
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????},?"消費(fèi)者");
          ????????consumer.start();
          ????}
          }

          程序的執(zhí)行結(jié)果如下圖所示:從上述結(jié)果可以看出,當(dāng)使用 Condition?時(shí),生產(chǎn)者、消費(fèi)者、生產(chǎn)者 2 會(huì)一直交替循環(huán)執(zhí)行,執(zhí)行結(jié)果符合我們的預(yù)期。

          2.性能問題

          在上面我們演示 notify?會(huì)造成線程的“假死”問題的時(shí)候,一定有朋友會(huì)想到,如果把 notify?換成 notifyAll?線程就不會(huì)“假死”了。

          這樣做法確實(shí)可以解決線程“假死”的問題,但同時(shí)會(huì)到來新的性能問題,空說無憑,直接上代碼展示。

          以下是使用 waitnotifyAll 改進(jìn)后的代碼:

          /**
          ?*?工廠類,消費(fèi)者和生產(chǎn)者通過調(diào)用工廠類實(shí)現(xiàn)生產(chǎn)/消費(fèi)功能.
          ?*/

          class?Factory?{
          ????private?int[]?items?=?new?int[1];???//?數(shù)據(jù)存儲(chǔ)容器(為了演示方便,設(shè)置容量最多存儲(chǔ)?1?個(gè)元素)
          ????private?int?size?=?0;???????????????//?實(shí)際存儲(chǔ)大小

          ????/**
          ?????*?生產(chǎn)方法
          ?????*?@throws?InterruptedException
          ?????*/

          ????public?synchronized?void?put()?throws?InterruptedException?{
          ????????//?循環(huán)生產(chǎn)數(shù)據(jù)
          ????????do?{
          ????????????while?(size?==?items.length)?{?//?注意不能是?if?判斷
          ????????????????//?存儲(chǔ)的容量已經(jīng)滿了,阻塞等待消費(fèi)者消費(fèi)之后喚醒
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?進(jìn)入阻塞");
          ????????????????this.wait();
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?被喚醒");
          ????????????}
          ????????????System.out.println(Thread.currentThread().getName()?+?"?開始工作");
          ????????????items[0]?=?1;?//?為了方便演示,設(shè)置固定值
          ????????????size++;
          ????????????System.out.println(Thread.currentThread().getName()?+?"?完成工作");
          ????????????//?喚醒所有線程
          ????????????this.notifyAll();
          ????????}?while?(true);
          ????}

          ????/**
          ?????*?消費(fèi)方法
          ?????*?@throws?InterruptedException
          ?????*/

          ????public?synchronized?void?take()?throws?InterruptedException?{
          ????????//?循環(huán)消費(fèi)數(shù)據(jù)
          ????????do?{
          ????????????while?(size?==?0)?{
          ????????????????//?生產(chǎn)者沒有數(shù)據(jù),阻塞等待
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?進(jìn)入阻塞(消費(fèi)者)");
          ????????????????this.wait();
          ????????????????System.out.println(Thread.currentThread().getName()?+?"?被喚醒(消費(fèi)者)");
          ????????????}
          ????????????System.out.println("消費(fèi)者工作~");
          ????????????size--;
          ????????????//?喚醒所有線程
          ????????????this.notifyAll();
          ????????}?while?(true);
          ????}
          }

          依舊是兩個(gè)生產(chǎn)者加一個(gè)消費(fèi)者,實(shí)現(xiàn)代碼如下:

          public?static?void?main(String[]?args)?{
          ????Factory?factory?=?new?Factory();
          ????//?生產(chǎn)者
          ????Thread?producer?=?new?Thread(()?->?{
          ????????try?{
          ????????????factory.put();
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????},?"生產(chǎn)者");
          ????producer.start();

          ????//?生產(chǎn)者?2
          ????Thread?producer2?=?new?Thread(()?->?{
          ????????try?{
          ????????????factory.put();
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????},?"生產(chǎn)者2");
          ????producer2.start();

          ????//?消費(fèi)者
          ????Thread?consumer?=?new?Thread(()?->?{
          ????????try?{
          ????????????factory.take();
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????},?"消費(fèi)者");
          ????consumer.start();
          }

          執(zhí)行的結(jié)果如下圖所示:通過以上結(jié)果可以看出:當(dāng)我們調(diào)用 notifyAll?時(shí)確實(shí)不會(huì)造成線程“假死”了,但會(huì)造成所有的生產(chǎn)者都被喚醒了,但因?yàn)榇龍?zhí)行的任務(wù)只有一個(gè),因此被喚醒的所有生產(chǎn)者中,只有一個(gè)會(huì)執(zhí)行正確的工作,而另一個(gè)則是啥也不干,然后又進(jìn)入等待狀態(tài),這就行為對(duì)于整個(gè)程序來說,無疑是多此一舉,只會(huì)增加線程調(diào)度的開銷,從而導(dǎo)致整個(gè)程序的性能下降。

          反觀 Condition?的 await?和 signal?方法,即使有多個(gè)生產(chǎn)者,程序也只會(huì)喚醒一個(gè)有效的生產(chǎn)者進(jìn)行工作,如下圖所示:生產(chǎn)者和生產(chǎn)者 2 依次會(huì)被交替的喚醒進(jìn)行工作,所以這樣執(zhí)行時(shí)并沒有任何多余的開銷,從而相比于 notifyAll?而言整個(gè)程序的性能會(huì)提升不少。

          總結(jié)

          本文我們通過代碼和流程圖的方式演示了 wait?方法和 notify/notifyAll?方法的使用缺陷,它的缺陷主要有兩個(gè),一個(gè)是在極端環(huán)境下使用 notify?會(huì)造成程序“假死”的情況,另一個(gè)就是使用 notifyAll?會(huì)造成性能下降的問題,因此在進(jìn)行線程通訊時(shí),強(qiáng)烈建議使用 Condition?類來實(shí)現(xiàn)。

          PS:有人可能會(huì)問為什么不用 Condition 的 signalAll 和 notifyAll 進(jìn)行性能對(duì)比?而使用 signal 和 notifyAll 進(jìn)行對(duì)比?我只想說,既然使用 signal 可以實(shí)現(xiàn)此功能,為什么還要使用 signalAll 呢?這就好比在有暖氣的 25 度的房間里,穿一件短袖就可以了,為什么還要穿一件棉襖呢?


          往期推薦

          求求你,不要再使用!=null判空了!

          2020-12-01

          提高生產(chǎn)力,最全 MyBatisPlus 講解!

          2020-12-10

          2020年終總結(jié):新的“開始”

          2020-12-11


          關(guān)注我,每天陪你進(jìn)步一點(diǎn)點(diǎn)!

          瀏覽 35
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕在线观 | 亚洲免费看黄网站 | 国产女人叫床高潮大片免费 | 午夜成人免费网站 | 激情小说一区 |