Java多線程中wait、notify、notifyAll使用詳解
基礎(chǔ)知識(shí)
首先我們需要知道,這幾個(gè)都是Object對(duì)象的方法。換言之,Java中所有的對(duì)象都有這些方法。
public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait() throws InterruptedException { wait(0); }
其中notify()、notifyAll()、wait(long timeout)是本地方法
其次我們需要知道這幾個(gè)方法主要是用來(lái)個(gè)線程之間通信的。那可能就有人會(huì)問(wèn),既然是用來(lái)線程之間通信的,那為什么這幾個(gè)方法不是在線程類Thread上呢?對(duì)于這個(gè)問(wèn)題,我們先來(lái)看下這幾個(gè)方法的具體使用方式再來(lái)回答這個(gè)問(wèn)題。
用法
Java中規(guī)定,在調(diào)用者三個(gè)方法時(shí),當(dāng)前線程必須獲得對(duì)象鎖。因此就得配合synchronized關(guān)鍵字來(lái)使用
//使用模式,不代表可運(yùn)行代碼 synchronized(object) { while(contidion) { object.wait(); } //object.notify(); //object.notifyAll(); }
或者
public synchronized void methodName() { while(contidion) { object.wait(); } //object.notify(); //object.notifyAll(); }
在synchronized拿到對(duì)象鎖之后,synchronized代碼塊或者方法中,必定是會(huì)持有對(duì)象鎖的,因此就可以使用wait()或者notify()。
通過(guò)上述使用方法,我們也能很好理解為什么這幾個(gè)方法是在Object上而不是在Thread上。因?yàn)槊總€(gè)對(duì)象都可以作為synchronized鎖的對(duì)象,因此wait、notify等必須和對(duì)象關(guān)聯(lián)才能配合synchronized使用。
作用
方法 | 作用 |
---|---|
wait | 線程自動(dòng)釋放占有的對(duì)象鎖,并等待notify。 |
notify | 隨機(jī)喚醒一個(gè)正在wait當(dāng)前對(duì)象的線程,并讓被喚醒的線程拿到對(duì)象鎖 |
notifyAll | 喚醒所有正在wait當(dāng)前對(duì)象的線程,但是被喚醒的線程會(huì)再次去競(jìng)爭(zhēng)對(duì)象鎖。因?yàn)橐淮沃挥幸粋€(gè)線程能拿到鎖,所有其他沒(méi)有拿到鎖的線程會(huì)被阻塞。推薦使用。 |
1、wait()、notify/notifyAll() 方法是Object的本地final方法,無(wú)法被重寫。
2、wait()使當(dāng)前線程阻塞,前提是 必須先獲得鎖,一般配合synchronized 關(guān)鍵字使用,即,一般在synchronized 同步代碼塊里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代碼塊執(zhí)行,說(shuō)明當(dāng)前線程一定是獲取了鎖的。
當(dāng)線程執(zhí)行wait()方法時(shí)候,會(huì)釋放當(dāng)前的鎖,然后讓出CPU,進(jìn)入等待狀態(tài)。
只有當(dāng) notify/notifyAll() 被執(zhí)行時(shí)候,才會(huì)喚醒一個(gè)或多個(gè)正處于等待狀態(tài)的線程,然后繼續(xù)往下執(zhí)行,直到執(zhí)行完synchronized 代碼塊的代碼或是中途遇到wait() ,再次釋放鎖。
也就是說(shuō),notify/notifyAll() 的執(zhí)行只是喚醒沉睡的線程,而不會(huì)立即釋放鎖,鎖的釋放要看代碼塊的具體執(zhí)行情況。所以在編程中,盡量在使用了notify/notifyAll() 后立即退出臨界區(qū),以喚醒其他線程
4、wait() 需要被try catch包圍,中斷也可以使wait等待的線程喚醒。
5、notify 和wait 的順序不能錯(cuò),如果A線程先執(zhí)行notify方法,B線程在執(zhí)行wait方法,那么B線程是無(wú)法被喚醒的。
6、notify 和 notifyAll的區(qū)別
notify方法只喚醒一個(gè)等待(對(duì)象的)線程并使該線程開始執(zhí)行。所以如果有多個(gè)線程等待一個(gè)對(duì)象,這個(gè)方法只會(huì)喚醒其中一個(gè)線程,選擇哪個(gè)線程取決于操作系統(tǒng)對(duì)多線程管理的實(shí)現(xiàn)。notifyAll 會(huì)喚醒所有等待(對(duì)象的)線程,盡管哪一個(gè)線程將會(huì)第一個(gè)處理取決于操作系統(tǒng)的實(shí)現(xiàn)。如果當(dāng)前情況下有多個(gè)線程需要被喚醒,推薦使用notifyAll 方法。比如在生產(chǎn)者-消費(fèi)者里面的使用,每次都需要喚醒所有的消費(fèi)者或是生產(chǎn)者,以判斷程序是否可以繼續(xù)往下執(zhí)行。
7、在多線程中要測(cè)試某個(gè)條件的變化,使用if 還是while?
要注意,notify喚醒沉睡的線程后,線程會(huì)接著上次的執(zhí)行繼續(xù)往下執(zhí)行。所以在進(jìn)行條件判斷時(shí)候,可以先把 wait 語(yǔ)句忽略不計(jì)來(lái)進(jìn)行考慮,顯然,要確保程序一定要執(zhí)行,并且要保證程序直到滿足一定的條件再執(zhí)行,要使用while來(lái)執(zhí)行,以確保條件滿足和一定執(zhí)行。如下代碼:
public class K { //狀態(tài)鎖 private Object lock; //條件變量 private int now,need; public void produce(int num){ //同步 synchronized (lock){ //當(dāng)前有的不滿足需要,進(jìn)行等待 while(now < need){ try { //等待阻塞 wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我被喚醒了!"); } // 做其他的事情 } } }
顯然,只有當(dāng)前值滿足需要值的時(shí)候,線程才可以往下執(zhí)行,所以,必須使用while 循環(huán)阻塞。注意,wait() 當(dāng)被喚醒時(shí)候,只是讓while循環(huán)繼續(xù)往下走.如果此處用if的話,意味著if繼續(xù)往下走,會(huì)跳出if語(yǔ)句塊。但是,notifyAll 只是負(fù)責(zé)喚醒線程,并不保證條件云云,所以需要手動(dòng)來(lái)保證程序的邏輯。
實(shí)際案例
接下來(lái)我們就使用wait()、notify()來(lái)實(shí)現(xiàn)一個(gè)生產(chǎn)者、消費(fèi)者模式。這個(gè)也是面試過(guò)程中可能會(huì)被問(wèn)到的地方。至于什么是生產(chǎn)者消費(fèi)者模式,不明白的同學(xué)請(qǐng)自行百度。
首先是一些基礎(chǔ)的代碼
private static Boolean run = true;//控制是否生產(chǎn)和消費(fèi) private static final Integer MAX_CAPACITY = 5;//緩沖區(qū)最大數(shù)量 private static final LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();//緩沖隊(duì)列
生產(chǎn)者代碼
/** * 生產(chǎn)者 */ class Producter extends Thread { @Override public void run() { while (run) { synchronized (queue) { while (queue.size() >= MAX_CAPACITY * 2) { try { System.out.println("緩沖隊(duì)列已滿,等待消費(fèi)"); queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { String string = UUID.randomUUID().toString(); queue.put(string); System.out.println("生產(chǎn):" + string); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } queue.notifyAll();//通知生產(chǎn)者和消費(fèi)者 } } } }
消費(fèi)者代碼
/** * 消費(fèi)者 */ class Consumer extends Thread { @Override public void run() { while (run) { synchronized (queue) { while (queue.isEmpty()) { try { System.out.println("隊(duì)列為空,等待生產(chǎn)"); queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { System.out.println("消費(fèi):" + queue.take()); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } queue.notifyAll();//通知生產(chǎn)者和消費(fèi)者 } } } }
代碼說(shuō)明
1、生產(chǎn)者和消費(fèi)者都繼承了線程Thread,因?yàn)閣ait、notify本身就是線程間通信使用
2、生產(chǎn)者和消費(fèi)者都有兩層while,外層的while是用來(lái)判斷是否運(yùn)行生產(chǎn)者和消費(fèi)者。內(nèi)存的while用來(lái)判斷隊(duì)列queue是否已滿或者為空,如果滿足條件,則使得當(dāng)前線程變成等待狀態(tài)(等待notify)。
3、內(nèi)層的條件判斷為什么用while不用if,原因是當(dāng)線程被wait之后,會(huì)釋放對(duì)象鎖。當(dāng)?shù)却木€程被notify之后,必須再次嘗試去獲取對(duì)象鎖,如果沒(méi)有獲取到對(duì)象鎖,那還必須等待,直到拿到對(duì)象鎖之后才能向后執(zhí)行。
4、當(dāng)生產(chǎn)者生產(chǎn)了一個(gè)數(shù)據(jù)或者消費(fèi)者消費(fèi)了一個(gè)數(shù)據(jù)之后,使用notifyAll()方法來(lái)通知所有等待當(dāng)前對(duì)象鎖的線程,但是一次只會(huì)有一個(gè)等待的線程能拿到鎖。
5、我們使用queue作為鎖的對(duì)象在不同線程之間進(jìn)行通信
代碼運(yùn)行結(jié)果
隊(duì)列為空,等待生產(chǎn)
生產(chǎn):e422484e-8eb3-4a91-8a7c-97e21d7ef498
生產(chǎn):7894b802-2529-4798-ba98-9f0657f39240
生產(chǎn):848f6759-a427-4a94-89dc-3f484daaa467
生產(chǎn):f711d3dc-972c-4c44-8640-faffe376d354
生產(chǎn):38a08e62-d774-4ed5-8b51-f1ad534c246f
消費(fèi):e422484e-8eb3-4a91-8a7c-97e21d7ef498
消費(fèi):7894b802-2529-4798-ba98-9f0657f39240
消費(fèi):848f6759-a427-4a94-89dc-3f484daaa467
消費(fèi):f711d3dc-972c-4c44-8640-faffe376d354
消費(fèi):38a08e62-d774-4ed5-8b51-f1ad534c246f
隊(duì)列為空,等待生產(chǎn)
生產(chǎn):9fae26f3-0b6e-4fbd-9620-040667efe0af
生產(chǎn):95bb1d88-e08a-4f70-a270-f75760994184
消費(fèi):9fae26f3-0b6e-4fbd-9620-040667efe0af
消費(fèi):95bb1d88-e08a-4f70-a270-f75760994184
隊(duì)列為空,等待生產(chǎn)
生產(chǎn):13d304bc-dff3-44a4-9527-2e0facd884e7
生產(chǎn):2693e069-bae1-4beb-adcd-a3c3bf5d232b
消費(fèi):13d304bc-dff3-44a4-9527-2e0facd884e7
消費(fèi):2693e069-bae1-4beb-adcd-a3c3bf5d232b
由結(jié)果也可以看出來(lái),生產(chǎn)和消費(fèi)是無(wú)規(guī)則的,因?yàn)殡m然notifyAll()通知了所有的等待線程,但是不確定那個(gè)線程中能拿到對(duì)象鎖。但是也有一個(gè)很明顯的問(wèn)題,因?yàn)橥瑫r(shí)只有一個(gè)線程能拿到對(duì)象鎖,生產(chǎn)者和消費(fèi)者不可能同時(shí)運(yùn)行。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java設(shè)計(jì)模式之策略模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
策略模式是對(duì)算法的封裝,把一系列的算法分別封裝到對(duì)應(yīng)的類中,并且這些類實(shí)現(xiàn)相同的接口,相互之間可以替換。接下來(lái)通過(guò)本文給大家分享Java設(shè)計(jì)模式之策略模式,感興趣的朋友一起看看吧2017-08-08Java判斷List中相同值元素的個(gè)數(shù)實(shí)例
今天小編就為大家分享一篇Java判斷List中相同值元素的個(gè)數(shù)實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07

Springboot?整合maven插口調(diào)用maven?release?plugin實(shí)現(xiàn)一鍵打包功能

Java Spring5學(xué)習(xí)之JdbcTemplate詳解

springboot讀取文件,打成jar包后訪問(wèn)不到的解決