您好,登錄后才能下訂單哦!
這篇文章主要介紹java并發(fā)中wait notify notifyAll的示例分析,文中介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們一定要看完!
java 面試是否有被問到過,sleep
和 wait
方法的區(qū)別,關(guān)于這個問題其實不用多說,大多數(shù)人都能回答出最主要的兩點區(qū)別:
sleep 是線程的方法, wait / notify / notifyAll 是 Object 類的方法;
sleep 不會釋放當(dāng)前線程持有的鎖,到時間后程序會繼續(xù)執(zhí)行,wait
會釋放線程持有的鎖并掛起,直到通過 notify
或者 notifyAll
重新獲得鎖。
另外還有一些參數(shù)、異常等區(qū)別,不細(xì)說了。本文重點記錄一下 wait / notify / notifyAll 的相關(guān)知識。
開發(fā)中常常遇到這樣的場景:
一個線程執(zhí)行過程中,需要開啟另外一個子線程去做某個耗時的操作(通過休眠3秒模擬),并且**等待**子線程返回結(jié)果,主線程再根據(jù)返回的結(jié)果繼續(xù)往下執(zhí)行。
這里注意我上面加*兩個字“等待”。如果不需要等待,單純只是對子線程的結(jié)果做處理,我們大可注冊回調(diào)方法解決問題,此文不再贅述接口回調(diào)。
此處場景就是主線程停下來等待子線程執(zhí)行完畢后,主線程再繼續(xù)執(zhí)行。針對該場景下面給出實現(xiàn):
volatile boolean flag = false; public void test(){ //... Thread t1 = new Thread(() -> { try { Thread.sleep(3000); System.out.println("--- 休眠 3 秒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { flag = true; } }); t1.start(); while(!flag){ } System.out.println("--- work thread run"); }
上面的代碼,執(zhí)行結(jié)果:
強(qiáng)調(diào)一點,聲明標(biāo)志位的時候,一定注意 volatile
關(guān)鍵字不能忘,如果不加該關(guān)鍵字修飾,程序可能進(jìn)入死循環(huán)。這是同步中的可見性問題,在 《java 并發(fā)——內(nèi)置鎖》 中有記錄。
顯然,這個實現(xiàn)方案并不好,本來主線程什么也不用做,卻一直在競爭資源,做空循環(huán),性能上不好,所以并不推薦。
public void test(){ //... Thread t1 = new Thread(() -> { try { Thread.sleep(3000); System.out.println("--- 休眠 3 秒"); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("--- work thread continue"); }
上面的代碼,執(zhí)行結(jié)果同上。利用 Thread 類的 join 方法實現(xiàn)了同步,達(dá)到了效果,但是 join 方法不能一定保證效果,在不同的 cpu 上,可能呈現(xiàn)出意想不到的結(jié)果,所以盡量不要用上述方法。
不清楚閉鎖的新同學(xué)可以了解下java并發(fā)中的線程。
public void test(){ //... final CountDownLatch countDownLatch = new CountDownLatch(1); Thread t1 = new Thread(() -> { try { Thread.sleep(3000); System.out.println("--- 休眠 3 秒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } }); t1.start(); try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("--- work thread run"); }
上面的代碼,執(zhí)行結(jié)果同上。同樣可以實現(xiàn)上述效果,執(zhí)行結(jié)果和上面一樣。該方法推薦使用。
為了方便對比,首先給 2.1 中的循環(huán)方法增加一些打印。修改后的代碼如下:
volatile boolean flag = false; public void test() { //... Thread t1 = new Thread(() -> { try { Thread.sleep(3000); System.out.println("--- 休眠 3 秒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { flag = true; } }); t1.start(); while (!flag) { try { System.out.println("---while-loop---"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("--- work thread run"); }
執(zhí)行結(jié)果如下:
事實證明,while 循環(huán)確實一直在執(zhí)行。
為了使該線程再不需要執(zhí)行的時候不搶占資源,我們可以利用 wait
方法將其掛起,在需要它執(zhí)行的時候,再利用 notify
方法將其喚醒。這樣達(dá)到優(yōu)化的目的,優(yōu)化后的代碼如下:
volatile boolean flag = false; public void test() { //... final Object obj = new Object(); Thread t1 = new Thread(() -> { synchronized (obj) { try { Thread.sleep(3000); System.out.println("--- 休眠 3 秒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { flag = true; } obj.notify(); } }); t1.start(); synchronized (obj) { while (!flag) { try { System.out.println("---while-loop---"); Thread.sleep(500); obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println("--- work thread run"); }
執(zhí)行結(jié)果:
結(jié)果證明,優(yōu)化后的程序,循環(huán)只執(zhí)行了一次。
在Java中,每個對象都有兩個池,鎖(monitor)池和等待池
鎖池:假設(shè)線程A已經(jīng)擁有了某個對象的鎖,而其它的線程想要調(diào)用這個對象的某個synchronized方法(或者synchronized塊),由于這些線程在進(jìn)入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權(quán),但是該對象的鎖目前正被線程A擁有,所以這些線程就進(jìn)入了該對象的鎖池中。
等待池:假設(shè)一個線程A調(diào)用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因為wait()方法必須出現(xiàn)在synchronized中,這樣自然在執(zhí)行wait()方法之前線程A就已經(jīng)擁有了該對象的鎖),同時線程A就進(jìn)入到了該對象的等待池中。如果另外的一個線程調(diào)用了相同對象的notifyAll()方法,那么處于該對象的等待池中的線程就會全部進(jìn)入該對象的鎖池中,準(zhǔn)備爭奪鎖的擁有權(quán)。如果另外的一個線程調(diào)用了相同對象的notify()方法,那么僅僅有一個處于該對象的等待池中的線程(隨機(jī))會進(jìn)入該對象的鎖池.
public final void wait() throws InterruptedException,IllegalMonitorStateException
該方法用來將當(dāng)前線程置入休眠狀態(tài),直到接到通知或被中斷為止。在調(diào)用 wait()之前,線程必須要獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調(diào)用 wait()方法。進(jìn)入 wait()方法后,當(dāng)前線程釋放鎖。在從 wait()返回前,線程與其他線程競爭重新獲得鎖。如果調(diào)用 wait()時,沒有持有適當(dāng)?shù)逆i,則拋出 IllegalMonitorStateException,它是 RuntimeException 的一個子類,因此,不需要 try-catch 結(jié)
public final native void notify() throws IllegalMonitorStateException
該方法也要在同步方法或同步塊中調(diào)用,即在調(diào)用前,線程也必須要獲得該對象的對象級別鎖,的如果調(diào)用 notify()時沒有持有適當(dāng)?shù)逆i,也會拋出 IllegalMonitorStateException。
該方法用來通知那些可能等待該對象的對象鎖的其他線程。如果有多個線程等待,則線程規(guī)劃器任意挑選出其中一個 wait()狀態(tài)的線程來發(fā)出通知,并使它等待獲取該對象的對象鎖(notify 后,當(dāng)前線程不會馬上釋放該對象鎖,wait 所在的線程并不能馬上獲取該對象鎖,要等到程序退出 synchronized 代碼塊后,當(dāng)前線程才會釋放鎖,wait所在的線程也才可以獲取該對象鎖),但不驚動其他同樣在等待被該對象notify的線程們。當(dāng)?shù)谝粋€獲得了該對象鎖的 wait 線程運行完畢以后,它會釋放掉該對象鎖,此時如果該對象沒有再次使用 notify 語句,則即便該對象已經(jīng)空閑,其他 wait 狀態(tài)等待的線程由于沒有得到該對象的通知,會繼續(xù)阻塞在 wait 狀態(tài),直到這個對象發(fā)出一個 notify 或 notifyAll。這里需要注意:它們等待的是被 notify 或 notifyAll,而不是鎖。這與下面的 notifyAll()方法執(zhí)行后的情況不同。
public final native void notifyAll() throws IllegalMonitorStateException
該方法與 notify ()方法的工作方式相同,重要的一點差異是:
notifyAll 使所有原來在該對象上 wait 的線程統(tǒng)統(tǒng)退出 wait 的狀態(tài)(即全部被喚醒,不再等待 notify 或 notifyAll,但由于此時還沒有獲取到該對象鎖,因此還不能繼續(xù)往下執(zhí)行),變成等待獲取該對象上的鎖,一旦該對象鎖被釋放(notifyAll 線程退出調(diào)用了 notifyAll 的 synchronized 代碼塊的時候),他們就會去競爭。如果其中一個線程獲得了該對象鎖,它就會繼續(xù)往下執(zhí)行,在它退出 synchronized 代碼塊,釋放鎖后,其他的已經(jīng)被喚醒的線程將會繼續(xù)競爭獲取該鎖,一直進(jìn)行下去,直到所有被喚醒的線程都執(zhí)行完畢。
生產(chǎn)者與消費者問題是并發(fā)編程里面的經(jīng)典問題。接下來說說利用wait()和notify()來實現(xiàn)生產(chǎn)者和消費者并發(fā)問題:
顯然要保證生產(chǎn)者和消費者并發(fā)運行不出亂,主要要解決:當(dāng)生產(chǎn)者線程的緩存區(qū)為滿的時候,就應(yīng)該調(diào)用wait()來停止生產(chǎn)者繼續(xù)生產(chǎn),而當(dāng)生產(chǎn)者滿的緩沖區(qū)被消費者消費掉一塊時,則應(yīng)該調(diào)用notify()喚醒生產(chǎn)者,通知他可以繼續(xù)生產(chǎn);同樣,對于消費者,當(dāng)消費者線程的緩存區(qū)為空的時候,就應(yīng)該調(diào)用wait()停掉消費者線程繼續(xù)消費,而當(dāng)生產(chǎn)者又生產(chǎn)了一個時就應(yīng)該調(diào)用notify()來喚醒消費者線程通知他可以繼續(xù)消費了。
下面是一個簡單的代碼實現(xiàn):
package com.sharpcj; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) { Reposity reposity = new Reposity(600); ExecutorService threadPool = Executors.newCachedThreadPool(); for(int i = 0; i < 10; i++){ threadPool.submit(new Producer(reposity)); } for(int i = 0; i < 10; i++){ threadPool.submit(new Consumer(reposity)); } threadPool.shutdown(); } } class Reposity { private static final int MAX_NUM = 2000; private int currentNum; private final Object obj = new Object(); public Reposity(int currentNum) { this.currentNum = currentNum; } public void in(int inNum) { synchronized (obj) { while (currentNum + inNum > MAX_NUM) { try { System.out.println("入貨量 " + inNum + " 線程 " + Thread.currentThread().getId() + "被掛起..."); obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } currentNum += inNum; System.out.println("線程: " + Thread.currentThread().getId() + ",入貨:inNum = [" + inNum + "], currentNum = [" + currentNum + "]"); obj.notifyAll(); } } public void out(int outNum) { synchronized (obj) { while (currentNum < outNum) { try { System.out.println("出貨量 " + outNum + " 線程 " + Thread.currentThread().getId() + "被掛起..."); obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } currentNum -= outNum; System.out.println("線程: " + Thread.currentThread().getId() + ",出貨:outNum = [" + outNum + "], currentNum = [" + currentNum + "]"); obj.notifyAll(); } } } class Producer implements Runnable { private Reposity reposity; public Producer(Reposity reposity) { this.reposity = reposity; } @Override public void run() { reposity.in(200); } } class Consumer implements Runnable { private Reposity reposity; public Consumer(Reposity reposity) { this.reposity = reposity; } @Override public void run() { reposity.out(200); } }
執(zhí)行結(jié)果:
1.調(diào)用wait方法和notify、notifyAll方法前必須獲得對象鎖,也就是必須寫在synchronized(鎖對象){......}代碼塊中。
2.當(dāng)線程調(diào)用了wait方法后就釋放了對象鎖,否則其他線程無法獲得對象鎖。
3.當(dāng)調(diào)用 wait() 方法后,線程必須再次獲得對象鎖后才能繼續(xù)執(zhí)行。
4.如果另外兩個線程都在 wait,則正在執(zhí)行的線程調(diào)用notify方法只能喚醒一個正在wait的線程(公平競爭,由JVM決定)。
5.當(dāng)使用notifyAll方法后,所有wait狀態(tài)的線程都會被喚醒,但是只有一個線程能獲得鎖對象,必須執(zhí)行完while(condition){this.wait();}后才釋放對象鎖。其余的需要等待該獲得對象鎖的線程執(zhí)行完釋放對象鎖后才能繼續(xù)執(zhí)行。
6.當(dāng)某個線程調(diào)用notifyAll方法后,雖然其他線程被喚醒了,但是該線程依然持有著對象鎖,必須等該同步代碼塊執(zhí)行完(右大括號結(jié)束)后才算正式釋放了鎖對象,另外兩個線程才有機(jī)會執(zhí)行。
7.第5點中說明, wait 方法的調(diào)用前的條件判斷需放在循環(huán)中,否則可能出現(xiàn)邏輯錯誤。另外,根據(jù)程序邏輯合理使用 wait 即 notify 方法,避免如先執(zhí)行 notify ,后執(zhí)行 wait 方法,線程一直掛起之類的錯誤。
以上是“java并發(fā)中wait notify notifyAll的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。