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

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

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

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

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

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

輸出“Hello main”語句后停頓了兩秒,輸出“Hello thread”線程。
重點:
- wait、notify、notifyAll都是 Object 類的方法
- wait、notify、notifyAll 必須搭配 synchronized 關(guān)鍵字來使用
- 不帶參數(shù)的 wait 方法會造成死等、帶參數(shù)的 wait 方法則不會
- wait 方法的初心就是為了線程處于等待、阻塞狀態(tài)
- notify 方法的初心就是為了喚醒同一對象調(diào)用 wait 方法的隨機一個線程
- notifyAll 方法的初心就是為了喚醒同一對象調(diào)用 wait 方法的所有線程
到此這篇關(guān)于Java多線程中的wait與notify方法詳解的文章就介紹到這了,更多相關(guān)Java的wait與notify內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java Chassis3過載狀態(tài)下的快速失敗解決分析
本文解密了Java Chassis 3快速失敗相關(guān)的機制和背后故事,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01
解決springboot responseentity<string>亂碼問題
這篇文章主要介紹了解決springboot responseentity<string>亂碼問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07
深入分析JAVA Synchronized關(guān)鍵字
這篇文章主要介紹了析JAVA Synchronized關(guān)鍵字的相關(guān)知識,文中代碼非常詳細,幫助大家更好的理解和學習,感興趣的朋友可以了解下2020-06-06
httpclient模擬post請求json封裝表單數(shù)據(jù)的實現(xiàn)方法
下面小編就為大家?guī)硪黄猦ttpclient模擬post請求json封裝表單數(shù)據(jù)的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-12-12
解讀@NoArgsConstructor,@AllArgsConstructor,@RequiredArgsConstr
這篇文章主要介紹了解讀@NoArgsConstructor,@AllArgsConstructor,@RequiredArgsConstructor的區(qū)別及在springboot常用地方,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
spring boot中xalan引入報錯系統(tǒng)找不到指定的文件原因分析
這篇文章主要介紹了spring boot中xalan引入報錯系統(tǒng)找不到指定的文件,主要原因是內(nèi)嵌的tomcat9.0.36,本文給大家分享最新解決方法,需要的朋友可以參考下2023-08-08
Java使用Optional實現(xiàn)優(yōu)雅避免空指針異常
空指針異常(NullPointerException)可以說是Java程序員最容易遇到的問題了。為了解決這個問題,Java?8?版本中推出了?Optional?類,本文就來講講如何使用Optional實現(xiàn)優(yōu)雅避免空指針異常吧2023-03-03

