Java多線程中的wait與notify方法詳解
前言
我們知道,線程的調(diào)度是無序的,但有些情況要求線程的執(zhí)行是有序的。
因此,我們可以使用 wait() 方法來使線程執(zhí)行有序。
本期講解 Java 多線程中 synchronized 鎖配套使用的 wait 方法、notify方法和notifyAll方法,以及 wait 方法與 sleep 方法之間的區(qū)別、為什么要使用 wait 和 notify 方法。
為什么要使用wait()方法和notify()方法?
當(dāng)我們的 Java 代碼使用 synchronized 進(jìn)行加鎖時(shí),會(huì)出現(xiàn)線程之間搶占資源的情況。
這樣就會(huì)導(dǎo)致某一個(gè)線程不符合條件卻反復(fù)搶到資源,其他線程參與不了資源。
因此得使用 wait() 方法與 notify() 方法來解決該問題。
通過現(xiàn)實(shí)生活中的經(jīng)歷舉一例子:
把三個(gè)線程比做人,把一臺(tái) ATM 機(jī)比作鎖(synchronized)。
當(dāng)這三個(gè)線程去取錢時(shí),線程1優(yōu)先進(jìn)入了 ATM 機(jī)里面取錢。
當(dāng) ATM 里面沒有錢時(shí),線程1就出了 ATM 機(jī)。但由于線程離開了 ATM 機(jī)后,會(huì)一直與線程2和線程3搶占 ATM 機(jī),因此會(huì)造成一個(gè)極端的后果,就是線程1一直進(jìn)入 ATM 機(jī)然后出 ATM 機(jī),并且一直循環(huán)下去。

以上例子,線程1發(fā)現(xiàn) ATM 沒錢可取,卻還是反復(fù)進(jìn)出 ATM 這樣這樣其他線程就無法嘗試取錢,對(duì)應(yīng)的就是多線程中的多個(gè)線程競(jìng)爭(zhēng)鎖(synchroized)的情況,如何解決以上問題。
使用 wait 方法和 notify 方法。當(dāng) ATM(synchronized) 內(nèi)使用了 wait 方法,線程1取不了錢就會(huì)取消鎖狀態(tài)并且處于等待狀態(tài),當(dāng)其他線程進(jìn)入 ATM 機(jī)并且取到了錢這時(shí)候就可以使用 notify 方法喚醒 線程1的等待狀態(tài),那么線程1又可以進(jìn)行取錢操作,也就是進(jìn)行鎖的競(jìng)爭(zhēng)。
在使用 wait 方法后,線程1發(fā)現(xiàn) ATM 里面沒有錢可取,就會(huì)通過 wait 方法來釋放鎖并且進(jìn)行阻塞等待(也就是暫時(shí)不參與 CPU 的調(diào)度、鎖的競(jìng)爭(zhēng)),這個(gè)時(shí)候線程2和線程3就能很好的參與取錢這個(gè)操作了。
當(dāng)其他線程 使用 notify 方法時(shí),發(fā)現(xiàn) ATM 里面又有錢可取了。因此就會(huì)喚醒線程1的阻塞等待,這時(shí)線程1又可以參與 ATM(鎖) 的競(jìng)爭(zhēng)。直到,所有的線程都取到錢為止。
那么使得上述三個(gè)線程能供協(xié)調(diào)的完成取錢這個(gè)工作,會(huì)用到三個(gè)方法:
- wait() 方法/帶參數(shù)的wait()方法 - 讓當(dāng)前線程進(jìn)入等待阻塞狀態(tài)
- notify() 方法 / notifyAll() 方法 - 喚醒當(dāng)前對(duì)象上等待的線程
注意:wait,notify、notifyAll都是 Object 類中的方法。
1. wait()方法
wait 方法使用后:會(huì)把當(dāng)前的執(zhí)行的線程進(jìn)行等待阻塞,然后釋放當(dāng)前線程的鎖狀態(tài),當(dāng)滿足了一定條件后就被喚醒,重新嘗試獲取這個(gè)鎖。
wait 結(jié)束條件的為:
- 其他線程調(diào)用該對(duì)象的 notify 方法,
- wait 等待時(shí)間超時(shí)(wait 方法提供了一個(gè)帶有參數(shù)的版本,可以指定等待時(shí)間)
- 其他線程調(diào)用該等待的線程的 interrupt 方法,導(dǎo)致 wait 拋出 InterruptedException 異常。
解釋:interrupt(),在一個(gè)線程中調(diào)用另一個(gè)線程的interrupt()方法,即會(huì)向那個(gè)線程發(fā)出信號(hào)—線程中斷狀態(tài)已被設(shè)置。至于那個(gè)線程何去何從,由具體的代碼實(shí)現(xiàn)決定。
wait 和 notify 方法是 Object 類里面的方法,只要是一個(gè)類對(duì)象都能調(diào)用這兩個(gè)方法。因此,我們可以寫出以下代碼:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("Hello object");
object.wait();
System.out.println("object結(jié)束");
}運(yùn)行后打印:

以上代碼運(yùn)行后打印出一個(gè)非法的警告:非法的鎖狀態(tài)異常,因?yàn)?wait 方法必須要搭配 synchronized 來使用,脫離了 synchronized 的前提下 使用 wait 就會(huì)出現(xiàn)報(bào)錯(cuò)。
2. notify()方法
notify 方法是喚醒等待的線程,也就是喚醒調(diào)用了 wait 方法的線程。
notify 方法作用:
- notify 方法也要在同步方法或同步塊中調(diào)用,該方法是用來通知那些可能等待該對(duì)象的對(duì)象鎖的
- 其它線程,對(duì)其發(fā)出通知notify,并使它們重新獲取該對(duì)象的對(duì)象鎖
- 如有多個(gè)線程處于等待,則線程調(diào)度器會(huì)隨機(jī)挑選一個(gè)調(diào)用 wait 狀態(tài)的線程。
- 在調(diào)用 notify 方法后,當(dāng)前線程不會(huì)立馬釋放該對(duì)象的鎖,要等當(dāng)前調(diào)用 notify 方法的線程執(zhí)行完畢后,才會(huì)釋放該對(duì)象的鎖。
在理解 wait 方法和 notify 方法的作用以及使用方法后,下面我們來看下 wait 方法和 notify 方法的結(jié)合使用。
3. wait()和notify()方法的使用
代碼案例:使用 notify() 方法喚醒 thread1線程。
- 實(shí)例化一個(gè) Object 類的對(duì)象,調(diào)用 wait 和 notify 方法都是用該對(duì)象的引用 object 來調(diào)用。
- 創(chuàng)建兩個(gè)線程:線程1和線程2,線程1執(zhí)行兩條語句,線程2也執(zhí)行兩條語句。
- 線程1內(nèi)使用 object 來調(diào)用 wait 方法(兩條語句中間調(diào)用)
- 線程2內(nèi)使用 object 來調(diào)用 notify 方法(兩條語句中間調(diào)用)
因此,有以下代碼:
public static void main(String[] args) {
Object object = new Object();
Thread thread1 = new Thread(()-> {
synchronized (object) {
System.out.println("thread1開始");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1結(jié)束");
}
});
thread1.start();//啟動(dòng)thread1線程
Thread thread2 = new Thread(()-> {
synchronized (object) {
System.out.println("thread2開始");
object.notify();
System.out.println("thread2結(jié)束");
}
});
thread2.start();//啟動(dòng)thread2線程
}運(yùn)行后打?。?/p>

以上代碼,輸出順序與需求有所差異,但最終還是達(dá)到了效果。
造成輸出順序的不規(guī)則原因?yàn)椋?/p>
當(dāng) thread1 線程被 wait 前打印了語句“thread1開始”,thread2 線程 中調(diào)用了 notify 方法,這時(shí)會(huì)喚醒 thread1 線程,但是前提得執(zhí)行完 thread2 中的內(nèi)容“thread2開始”、“thread2結(jié)束”這兩個(gè)條語句。隨后才輸出被喚醒的 thread1 線程中的“thread1結(jié)束”語句。
當(dāng)然,既然這樣為啥我們不使用 join() 方法呢,thread1 線程完全執(zhí)行完畢,再執(zhí)行 thread2線程呢?具體情況具體分析,當(dāng)我們的代碼需求滿足使用 join() 方法時(shí),我們就使用 join() 方法。
對(duì)應(yīng)上述代碼,join() 方法會(huì)使 thread1 線程執(zhí)行完畢后再執(zhí)行 thread2 線程。而 wait() 和 notify() 方法會(huì)使 thread1 線程執(zhí)行一部分后,執(zhí)行 thread2 線程,執(zhí)行完 thread2 一部分代碼后,再執(zhí)行thread1 線程。這樣就滿足了特定的條件,類似于上文中線程取錢情況。大家可以自行嘗試一番。
注意,wait() 方法的初心就是為了等待、阻塞的效果。在 synchronized 內(nèi)調(diào)用 wait() 方法,得按 Alt+Enter 這兩個(gè)組合鍵來 try/catch 異常。
4. notifyAll()方法
notifyAll() 方法是用來喚醒當(dāng)前對(duì)象的所有調(diào)用 wait() 的線程。案例:
- 有三個(gè)線程,線程1為thread1、線程2為thread2、線程3為thread3
- thread1 中輸出兩條語句“thread1開始”、“thread1結(jié)束”
- thread2 中輸出兩條語句“thread2開始”、“thread2結(jié)束”
- thread1 和 threa2 在兩條語句中間通過 Object 類的引用調(diào)用 wait() 方法造成阻塞
- thread3 線程通過 Object 類的引用調(diào)用 notifyAll() 方法喚醒所有的阻塞
因此,前兩個(gè)線程都通過 Object 類的引用調(diào)用了 wait() 方法造成阻塞,最后一個(gè)線程調(diào)用 notifyAll() 則喚醒了所有調(diào)用 wait() 方法的線程,如以下代碼:
public static void main(String[] args) {
Object object = new Object();//實(shí)例化一個(gè)Object類的對(duì)象
Thread thread1 = new Thread(()->{
synchronized (object) {
System.out.println("thread1-開始");
try {
object.wait();//thread1中調(diào)用wait方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1-結(jié)束");
}
});//創(chuàng)建thread1線程
thread1.start();//啟動(dòng)thread1線程
Thread thread2 = new Thread(()->{
synchronized(object) {
System.out.println("thread2-開始");
try {
object.wait();//thread2調(diào)用wait方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2-結(jié)束");
}
});//創(chuàng)建thread2線程
thread2.start();//啟動(dòng)thread2線程
Thread thread3 = new Thread(()->{
synchronized (object) {
object.notifyAll();//thread3中調(diào)用notifyAll方法
System.out.println("thread3調(diào)用了notifyAll方法");
}
});//創(chuàng)建thread3線程
thread3.start();//啟動(dòng)thread3線程
}運(yùn)行后打印:

以上代碼,通過 notifyAll() 方法喚醒了所有等待的線程。如果我把 notifyAll() 方法替換為 notify() 方法,此時(shí)就會(huì)隨機(jī)喚醒一個(gè)正在等待的線程。如以下打印結(jié)果:

通過上面截圖,我們可以觀察到隨機(jī)喚醒的是 thread1 線程。
5. wait()和sleep()的區(qū)別
wait 與 sleep 之間的區(qū)別:
- wait() 方法是 Object 類底下的方法,sleep() 方法是 Thread 類底下的靜態(tài)方法。
- wait()方法是搭配 synchronized 來使用的,而 sleep() 則不需要。
- 核心區(qū)別,初心不同,wait() 方法是為了避免線程之前的搶占資源(解決線程之間的順序控制),而 sleep() 方法是為了讓線程休眠特定的時(shí)間。
- wait() 方法有一個(gè)帶參數(shù)的寫法是用來體現(xiàn)超時(shí)的提醒(避免死等),因此用起來就感覺和 sleep() 方法一樣。
案例:
有兩線程,main 線程與 thread 線程,main 線程內(nèi)包含 thread 線程,main 線程內(nèi)有“Hello main”語句, thread 線程內(nèi)有“Hello thread”語句。
在 main 線程內(nèi)創(chuàng)建一個(gè) thread 線程,并且在 thread 線程內(nèi)使用 Object 類對(duì)象調(diào)用帶參的 wait() 方法,并設(shè)置參數(shù) 為2000。
在main 方法內(nèi)使用 Object 類對(duì)象調(diào)用 notify() 喚醒 thread 線程。使得輸出 main 線程內(nèi)語句后停頓兩秒輸出 thread 線程內(nèi)語句。
有以下代碼:
public static void main(String[] args) {
Object object = new Object();//實(shí)例化一個(gè)Object類對(duì)象
Thread thread = new Thread(()->{
synchronized (object) {
try {
object.wait(2000);//thread調(diào)用了帶參wait方法,停頓了兩秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello thread");
}
});//創(chuàng)建thread線程
thread.start();//啟動(dòng)thread線程
synchronized (object) {
object.notify();//main方法內(nèi)調(diào)用notify方法
}
System.out.println("Hello main");
}運(yùn)行后打印:

輸出“Hello main”語句后停頓了兩秒,輸出“Hello thread”線程。
重點(diǎn):
- wait、notify、notifyAll都是 Object 類的方法
- wait、notify、notifyAll 必須搭配 synchronized 關(guān)鍵字來使用
- 不帶參數(shù)的 wait 方法會(huì)造成死等、帶參數(shù)的 wait 方法則不會(huì)
- wait 方法的初心就是為了線程處于等待、阻塞狀態(tài)
- notify 方法的初心就是為了喚醒同一對(duì)象調(diào)用 wait 方法的隨機(jī)一個(gè)線程
- notifyAll 方法的初心就是為了喚醒同一對(duì)象調(diào)用 wait 方法的所有線程
到此這篇關(guān)于Java多線程中的wait與notify方法詳解的文章就介紹到這了,更多相關(guān)Java的wait與notify內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實(shí)現(xiàn)計(jì)算周期性提醒的示例
本文分享一個(gè)java實(shí)現(xiàn)計(jì)算周期性提醒的示例,可以計(jì)算父親節(jié)、母親節(jié)這樣的節(jié)日,也可以定義如每月最好一個(gè)周五,以方便安排會(huì)議2014-04-04
spring+Jpa多數(shù)據(jù)源配置的方法示例
這篇文章主要介紹了spring+Jpa多數(shù)據(jù)源配置的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
Java8的default和static關(guān)鍵字的使用講解
今天小編就為大家分享一篇關(guān)于Java8的default和static關(guān)鍵字的使用講解,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01
Java并發(fā)編程:volatile關(guān)鍵字詳細(xì)解析
這篇文章主要介紹了Java并發(fā)編程:volatile關(guān)鍵字詳細(xì)解析,對(duì)學(xué)習(xí)volatile關(guān)鍵字有一定的認(rèn)識(shí),有需要的可以了解一下。2016-11-11
Spring Boot+Shiro實(shí)現(xiàn)一個(gè)Http請(qǐng)求的Basic認(rèn)證
本文向向大家仔細(xì)的介紹了如何使用Shiro實(shí)現(xiàn)一個(gè)Http請(qǐng)求的Basic認(rèn)證,有此需求的朋友可以參考下本文2021-06-06
Java實(shí)現(xiàn)WGS84/百度/騰訊/高德等主流的地理坐標(biāo)轉(zhuǎn)換
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)WGS84,百度,騰訊,高德等主流的地理坐標(biāo)轉(zhuǎn)換,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2025-12-12

