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í),會出現(xiàn)線程之間搶占資源的情況。
這樣就會導(dǎo)致某一個(gè)線程不符合條件卻反復(fù)搶到資源,其他線程參與不了資源。
因此得使用 wait() 方法與 notify() 方法來解決該問題。
通過現(xiàn)實(shí)生活中的經(jīng)歷舉一例子:
把三個(gè)線程比做人,把一臺 ATM 機(jī)比作鎖(synchronized)。
當(dāng)這三個(gè)線程去取錢時(shí),線程1優(yōu)先進(jìn)入了 ATM 機(jī)里面取錢。
當(dāng) ATM 里面沒有錢時(shí),線程1就出了 ATM 機(jī)。但由于線程離開了 ATM 機(jī)后,會一直與線程2和線程3搶占 ATM 機(jī),因此會造成一個(gè)極端的后果,就是線程1一直進(jìn)入 ATM 機(jī)然后出 ATM 機(jī),并且一直循環(huán)下去。
以上例子,線程1發(fā)現(xiàn) ATM 沒錢可取,卻還是反復(fù)進(jìn)出 ATM 這樣這樣其他線程就無法嘗試取錢,對應(yīng)的就是多線程中的多個(gè)線程競爭鎖(synchroized)的情況,如何解決以上問題。
使用 wait 方法和 notify 方法。當(dāng) ATM(synchronized) 內(nèi)使用了 wait 方法,線程1取不了錢就會取消鎖狀態(tài)并且處于等待狀態(tài),當(dāng)其他線程進(jìn)入 ATM 機(jī)并且取到了錢這時(shí)候就可以使用 notify 方法喚醒 線程1的等待狀態(tài),那么線程1又可以進(jìn)行取錢操作,也就是進(jìn)行鎖的競爭。
在使用 wait 方法后,線程1發(fā)現(xiàn) ATM 里面沒有錢可取,就會通過 wait 方法來釋放鎖并且進(jìn)行阻塞等待(也就是暫時(shí)不參與 CPU 的調(diào)度、鎖的競爭),這個(gè)時(shí)候線程2和線程3就能很好的參與取錢這個(gè)操作了。
當(dāng)其他線程 使用 notify 方法時(shí),發(fā)現(xiàn) ATM 里面又有錢可取了。因此就會喚醒線程1的阻塞等待,這時(shí)線程1又可以參與 ATM(鎖) 的競爭。直到,所有的線程都取到錢為止。
那么使得上述三個(gè)線程能供協(xié)調(diào)的完成取錢這個(gè)工作,會用到三個(gè)方法:
- wait() 方法/帶參數(shù)的wait()方法 - 讓當(dāng)前線程進(jìn)入等待阻塞狀態(tài)
- notify() 方法 / notifyAll() 方法 - 喚醒當(dāng)前對象上等待的線程
注意:wait,notify、notifyAll都是 Object 類中的方法。
1. wait()方法
wait 方法使用后:會把當(dāng)前的執(zhí)行的線程進(jìn)行等待阻塞,然后釋放當(dāng)前線程的鎖狀態(tài),當(dāng)滿足了一定條件后就被喚醒,重新嘗試獲取這個(gè)鎖。
wait 結(jié)束條件的為:
- 其他線程調(diào)用該對象的 notify 方法,
- wait 等待時(shí)間超時(shí)(wait 方法提供了一個(gè)帶有參數(shù)的版本,可以指定等待時(shí)間)
- 其他線程調(diào)用該等待的線程的 interrupt 方法,導(dǎo)致 wait 拋出 InterruptedException 異常。
解釋:interrupt(),在一個(gè)線程中調(diào)用另一個(gè)線程的interrupt()方法,即會向那個(gè)線程發(fā)出信號—線程中斷狀態(tài)已被設(shè)置。至于那個(gè)線程何去何從,由具體的代碼實(shí)現(xiàn)決定。
wait 和 notify 方法是 Object 類里面的方法,只要是一個(gè)類對象都能調(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 就會出現(xiàn)報(bào)錯(cuò)。
2. notify()方法
notify 方法是喚醒等待的線程,也就是喚醒調(diào)用了 wait 方法的線程。
notify 方法作用:
- notify 方法也要在同步方法或同步塊中調(diào)用,該方法是用來通知那些可能等待該對象的對象鎖的
- 其它線程,對其發(fā)出通知notify,并使它們重新獲取該對象的對象鎖
- 如有多個(gè)線程處于等待,則線程調(diào)度器會隨機(jī)挑選一個(gè)調(diào)用 wait 狀態(tài)的線程。
- 在調(diào)用 notify 方法后,當(dāng)前線程不會立馬釋放該對象的鎖,要等當(dāng)前調(diào)用 notify 方法的線程執(zhí)行完畢后,才會釋放該對象的鎖。
在理解 wait 方法和 notify 方法的作用以及使用方法后,下面我們來看下 wait 方法和 notify 方法的結(jié)合使用。
3. wait()和notify()方法的使用
代碼案例:使用 notify() 方法喚醒 thread1線程。
- 實(shí)例化一個(gè) Object 類的對象,調(diào)用 wait 和 notify 方法都是用該對象的引用 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();//啟動thread1線程 Thread thread2 = new Thread(()-> { synchronized (object) { System.out.println("thread2開始"); object.notify(); System.out.println("thread2結(jié)束"); } }); thread2.start();//啟動thread2線程 }
運(yùn)行后打印:
以上代碼,輸出順序與需求有所差異,但最終還是達(dá)到了效果。
造成輸出順序的不規(guī)則原因?yàn)椋?/p>
當(dāng) thread1 線程被 wait 前打印了語句“thread1開始”,thread2 線程 中調(diào)用了 notify 方法,這時(shí)會喚醒 thread1 線程,但是前提得執(zhí)行完 thread2 中的內(nèi)容“thread2開始”、“thread2結(jié)束”這兩個(gè)條語句。隨后才輸出被喚醒的 thread1 線程中的“thread1結(jié)束”語句。
當(dāng)然,既然這樣為啥我們不使用 join() 方法呢,thread1 線程完全執(zhí)行完畢,再執(zhí)行 thread2線程呢?具體情況具體分析,當(dāng)我們的代碼需求滿足使用 join() 方法時(shí),我們就使用 join() 方法。
對應(yīng)上述代碼,join() 方法會使 thread1 線程執(zhí)行完畢后再執(zhí)行 thread2 線程。而 wait() 和 notify() 方法會使 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)前對象的所有調(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類的對象 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線程 }
運(yùn)行后打印:
以上代碼,通過 notifyAll() 方法喚醒了所有等待的線程。如果我把 notifyAll() 方法替換為 notify() 方法,此時(shí)就會隨機(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 類對象調(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();//實(shí)例化一個(gè)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"); }
運(yùn)行后打印:
輸出“Hello main”語句后停頓了兩秒,輸出“Hello thread”線程。
重點(diǎn):
- wait、notify、notifyAll都是 Object 類的方法
- wait、notify、notifyAll 必須搭配 synchronized 關(guān)鍵字來使用
- 不帶參數(shù)的 wait 方法會造成死等、帶參數(shù)的 wait 方法則不會
- wait 方法的初心就是為了線程處于等待、阻塞狀態(tài)
- notify 方法的初心就是為了喚醒同一對象調(diào)用 wait 方法的隨機(jī)一個(gè)線程
- 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)的機(jī)制和背后故事,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Java獲取當(dāng)前操作系統(tǒng)的信息實(shí)例代碼
這篇文章主要介紹了Java獲取當(dāng)前操作系統(tǒng)的信息實(shí)例代碼,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12解決springboot responseentity<string>亂碼問題
這篇文章主要介紹了解決springboot responseentity<string>亂碼問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07深入分析JAVA Synchronized關(guān)鍵字
這篇文章主要介紹了析JAVA Synchronized關(guān)鍵字的相關(guān)知識,文中代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06httpclient模擬post請求json封裝表單數(shù)據(jù)的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄猦ttpclient模擬post請求json封裝表單數(shù)據(jù)的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-12-12解讀@NoArgsConstructor,@AllArgsConstructor,@RequiredArgsConstr
這篇文章主要介紹了解讀@NoArgsConstructor,@AllArgsConstructor,@RequiredArgsConstructor的區(qū)別及在springboot常用地方,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12spring boot中xalan引入報(bào)錯(cuò)系統(tǒng)找不到指定的文件原因分析
這篇文章主要介紹了spring boot中xalan引入報(bào)錯(cuò)系統(tǒng)找不到指定的文件,主要原因是內(nèi)嵌的tomcat9.0.36,本文給大家分享最新解決方法,需要的朋友可以參考下2023-08-08Java使用Optional實(shí)現(xiàn)優(yōu)雅避免空指針異常
空指針異常(NullPointerException)可以說是Java程序員最容易遇到的問題了。為了解決這個(gè)問題,Java?8?版本中推出了?Optional?類,本文就來講講如何使用Optional實(shí)現(xiàn)優(yōu)雅避免空指針異常吧2023-03-03