這篇文章主要介紹“為什么不用Wait和Notify”,在日常操作中,相信很多人在為什么不用Wait和Notify問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”為什么不用Wait和Notify”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
根河網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián)公司,根河網(wǎng)站設(shè)計制作,有大型網(wǎng)站制作公司豐富經(jīng)驗。已為根河千余家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站制作要多少錢,請找那個售后服務(wù)好的根河做網(wǎng)站的公司定做!
1.notify 線程“假死”
所謂的線程“假死”是指,在使用 notify 喚醒多個等待的線程時,卻意外的喚醒了一個沒有“準備好”的線程,從而導致整個程序進入了阻塞的狀態(tài)不能繼續(xù)執(zhí)行。
以多線程編程中的經(jīng)典案例生產(chǎn)者和消費者模型為例,我們先來演示一下線程“假死”的問題。
1.1 正常版本
在演示線程“假死”的問題之前,我們先使用 wait 和 notify 來實現(xiàn)一個簡單的生產(chǎn)者和消費者模型,為了讓代碼更直觀,我這里寫一個超級簡單的實現(xiàn)版本。我們先來創(chuàng)建一個工廠類,工廠類里面包含兩個方法,一個是循環(huán)生產(chǎn)數(shù)據(jù)的(存入)方法,另一個是循環(huán)消費數(shù)據(jù)的(取出)方法,實現(xiàn)代碼如下。
/** * 工廠類,消費者和生產(chǎn)者通過調(diào)用工廠類實現(xiàn)生產(chǎn)/消費 */ class Factory { private int[] items = new int[1]; // 數(shù)據(jù)存儲容器(為了演示方便,設(shè)置容量最多存儲 1 個元素) private int size = 0; // 實際存儲大小 /** * 生產(chǎn)方法 */ public synchronized void put() throws InterruptedException { // 循環(huán)生產(chǎn)數(shù)據(jù) do { while (size == items.length) { // 注意不能是 if 判斷 // 存儲的容量已經(jīng)滿了,阻塞等待消費者消費之后喚醒 System.out.println(Thread.currentThread().getName() + " 進入阻塞"); 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() + " 完成工作"); // 當生產(chǎn)隊列有數(shù)據(jù)之后通知喚醒消費者 this.notify(); } while (true); } /** * 消費方法 */ public synchronized void take() throws InterruptedException { // 循環(huán)消費數(shù)據(jù) do { while (size == 0) { // 生產(chǎn)者沒有數(shù)據(jù),阻塞等待 System.out.println(Thread.currentThread().getName() + " 進入阻塞(消費者)"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被喚醒(消費者)"); } System.out.println("消費者工作~"); size--; // 喚醒生產(chǎn)者可以添加生產(chǎn)了 this.notify(); } while (true); } }
接下來我們來創(chuàng)建兩個線程,一個是生產(chǎn)者調(diào)用 put 方法,另一個是消費者調(diào)用 take 方法,實現(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(); // 消費者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消費者"); consumer.start(); } }
執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,生產(chǎn)者和消費者在循環(huán)交替的執(zhí)行任務(wù),場面非常和諧,是我們想要的正確結(jié)果。
1.2 線程“假死”版本
當只有一個生產(chǎn)者和一個消費者時,wait 和 notify 方法不會有任何問題,然而**將生產(chǎn)者增加到兩個時就會出現(xiàn)線程“假死”的問題了,**程序的實現(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(); // 生產(chǎn)者 2 Thread producer2 = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "生產(chǎn)者2"); producer2.start(); // 消費者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消費者"); consumer.start(); } }
程序執(zhí)行結(jié)果如下:
從以上結(jié)果可以看出,當我們將生產(chǎn)者的數(shù)量增加到 2 個時,就會造成線程“假死”阻塞執(zhí)行的問題,當生產(chǎn)者 2 被喚醒又被阻塞之后,整個程序就不能繼續(xù)執(zhí)行了。
線程“假死”問題分析
我們先把以上程序的執(zhí)行步驟標注一下,得到如下結(jié)果:
從上圖可以看出:當執(zhí)行到第 ④ 步時,此時生產(chǎn)者為工作狀態(tài),而生產(chǎn)者 2 和消費者為等待狀態(tài),此時正確的做法應(yīng)該是喚醒消費著進行消費,然后消費者消費完之后再喚醒生產(chǎn)者繼續(xù)工作;但此時生產(chǎn)者卻錯誤的喚醒了生產(chǎn)者 2,而生產(chǎn)者 2 因為隊列已經(jīng)滿了,所以自身并不具備繼續(xù)執(zhí)行的能力,因此就導致了整個程序的阻塞,流程圖如下所示:
正確執(zhí)行流程應(yīng)該是這樣的:
1.3 使用 Condition
為了解決線程的“假死”問題,我們可以使用 Condition 來嘗試實現(xiàn)一下,Condition 是 JUC(java.util.concurrent)包下的類,需要使用 Lock 鎖來創(chuàng)建,Condition 提供了 3 個重要的方法:
await:對應(yīng) wait 方法;
signal:對應(yīng) notify 方法;
signalAll: notifyAll 方法。
Condition 的使用和 wait/notify 類似,也是先獲得鎖然后在鎖中進行等待和喚醒操作,Condition 的基礎(chǔ)用法如下:
// 創(chuàng)建 Condition 對象 Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); // 加鎖 lock.lock(); try { // 業(yè)務(wù)方法.... // 1.進入等待狀態(tài) condition.await(); // 2.喚醒操作 condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); }
小知識:Lock的正確使用姿勢
切記 Lock 的 lock.lock() 方法不能放入 try 代碼中,如果 lock 方法在 try 代碼塊之內(nèi),可能由于其它方法拋出異常,導致在 finally 代碼塊中, unlock 對未加鎖的對象解鎖,它會調(diào)用 AQS 的 tryRelease 方法(取決于具體實現(xiàn)類),拋出 IllegalMonitorStateException 異常。
回歸主題
回到本文的主題,我們?nèi)绻褂?Condition 來實現(xiàn)線程的通訊就可以避免程序的“假死”情況,因為 Condition 可以創(chuàng)建多個等待集,以本文的生產(chǎn)者和消費者模型為例,我們可以使用兩個等待集,一個用做消費者的等待和喚醒,另一個用來喚醒生產(chǎn)者,這樣就不會出現(xiàn)生產(chǎn)者喚醒生產(chǎn)者的情況了(生產(chǎn)者只能喚醒消費者,消費者只能喚醒生產(chǎn)者)這樣整個流程就不會“假死”了,它的執(zhí)行流程如下圖所示:
了解了它的基本流程之后,咱們來看具體的實現(xiàn)代碼。
基于 Condition 的工廠實現(xiàn)代碼如下:
class FactoryByCondition { private int[] items = new int[1]; // 數(shù)據(jù)存儲容器(為了演示方便,設(shè)置容量最多存儲 1 個元素) private int size = 0; // 實際存儲大小 // 創(chuàng)建 Condition 對象 private Lock lock = new ReentrantLock(); // 生產(chǎn)者的 Condition 對象 private Condition producerCondition = lock.newCondition(); // 消費者的 Condition 對象 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)者進入等待 System.out.println(Thread.currentThread().getName() + " 進入阻塞"); 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() + " 完成工作"); // 喚醒消費者 consumerCondition.signal(); try { } finally { lock.unlock(); } } while (true); } /** * 消費方法 */ public void take() throws InterruptedException { // 循環(huán)消費數(shù)據(jù) do { lock.lock(); while (size == 0) { // 消費者阻塞等待 consumerCondition.await(); } System.out.println("消費者工作~"); size--; // 喚醒生產(chǎn)者 producerCondition.signal(); try { } finally { lock.unlock(); } } while (true); } }
兩個生產(chǎn)者和一個消費者的實現(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(); // 消費者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消費者"); consumer.start(); } }
程序的執(zhí)行結(jié)果如下圖所示:
從上述結(jié)果可以看出,當使用 Condition 時,生產(chǎn)者、消費者、生產(chǎn)者 2 會一直交替循環(huán)執(zhí)行,執(zhí)行結(jié)果符合我們的預期。
2.性能問題
在上面我們演示 notify 會造成線程的“假死”問題的時候,一定有朋友會想到,如果把 notify 換成 notifyAll 線程就不會“假死”了。
這樣做法確實可以解決線程“假死”的問題,但同時會到來新的性能問題,空說無憑,直接上代碼展示。
以下是使用 wait 和 notifyAll 改進后的代碼:
/** * 工廠類,消費者和生產(chǎn)者通過調(diào)用工廠類實現(xiàn)生產(chǎn)/消費功能. */ class Factory { private int[] items = new int[1]; // 數(shù)據(jù)存儲容器(為了演示方便,設(shè)置容量最多存儲 1 個元素) private int size = 0; // 實際存儲大小 /** * 生產(chǎn)方法 * @throws InterruptedException */ public synchronized void put() throws InterruptedException { // 循環(huán)生產(chǎn)數(shù)據(jù) do { while (size == items.length) { // 注意不能是 if 判斷 // 存儲的容量已經(jīng)滿了,阻塞等待消費者消費之后喚醒 System.out.println(Thread.currentThread().getName() + " 進入阻塞"); 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); } /** * 消費方法 * @throws InterruptedException */ public synchronized void take() throws InterruptedException { // 循環(huán)消費數(shù)據(jù) do { while (size == 0) { // 生產(chǎn)者沒有數(shù)據(jù),阻塞等待 System.out.println(Thread.currentThread().getName() + " 進入阻塞(消費者)"); this.wait(); System.out.println(Thread.currentThread().getName() + " 被喚醒(消費者)"); } System.out.println("消費者工作~"); size--; // 喚醒所有線程 this.notifyAll(); } while (true); } }
依舊是兩個生產(chǎn)者加一個消費者,實現(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(); // 消費者 Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "消費者"); consumer.start(); }
執(zhí)行的結(jié)果如下圖所示:
通過以上結(jié)果可以看出:當我們調(diào)用 notifyAll 時確實不會造成線程“假死”了,但會造成所有的生產(chǎn)者都被喚醒了,但因為待執(zhí)行的任務(wù)只有一個,因此被喚醒的所有生產(chǎn)者中,只有一個會執(zhí)行正確的工作,而另一個則是啥也不干,然后又進入等待狀態(tài),這種行為對于整個程序來說,無疑是多此一舉,只會增加線程調(diào)度的開銷,從而導致整個程序的性能下降。
反觀 Condition 的 await 和 signal 方法,即使有多個生產(chǎn)者,程序也只會喚醒一個有效的生產(chǎn)者進行工作,如下圖所示:
生產(chǎn)者和生產(chǎn)者 2 依次會被交替的喚醒進行工作,所以這樣執(zhí)行時并沒有任何多余的開銷,從而相比于 notifyAll 而言整個程序的性能會提升不少。
到此,關(guān)于“為什么不用Wait和Notify”的學習就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
網(wǎng)站標題:為什么不用Wait和Notify
URL地址:http://weahome.cn/article/ghspjp.html