Java多線(xiàn)程中的wait/notify通信模式實(shí)例詳解
前言
最近在看一些JUC下的源碼,更加意識(shí)到想要學(xué)好Java多線(xiàn)程,基礎(chǔ)是關(guān)鍵,比如想要學(xué)好ReentranLock源碼,就得掌握好AQS源碼,而AQS源碼中又有很多Java多線(xiàn)程經(jīng)典的一些應(yīng)用;再比如看了線(xiàn)程池的核心源碼實(shí)現(xiàn),又學(xué)到了很多核心實(shí)現(xiàn),其實(shí)這些都可以提出來(lái)慢慢消化并變成自己的知識(shí)點(diǎn),今天這個(gè)Java等待/通知模式其實(shí)是Thread.join()實(shí)現(xiàn)的關(guān)鍵,還有線(xiàn)程池工作線(xiàn)程中線(xiàn)程跟線(xiàn)程之間的通信的核心所在,故在此為了加深理解,做此記錄!
一、什么是Java線(xiàn)程的等待/通知模式
1、等待/通知模式概述
首先先介紹下官方的一個(gè)正式的介紹:
等待/通知機(jī)制,是指一個(gè)線(xiàn)程A調(diào)用了對(duì)象object的wait()方法進(jìn)入等待狀態(tài),而另一個(gè)線(xiàn)程B調(diào)用了對(duì)象object的notify或者notifyAll()方法,線(xiàn)程A收到通知后從對(duì)象O的wait()方法返回,進(jìn)而還行后續(xù)操作。
而我的理解是(舉例說(shuō)明):
假設(shè)工廠里有兩條流水線(xiàn),某個(gè)工作流程需要這兩個(gè)流水線(xiàn)配合完成,這兩個(gè)流水線(xiàn)分別是A和B,其中A負(fù)責(zé)準(zhǔn)備各種配件,B負(fù)責(zé)租裝配件之后產(chǎn)出輸出到工作臺(tái)。B的工作需要A的配件準(zhǔn)備充分,否則就會(huì)一直等待A準(zhǔn)備好配件,并且A準(zhǔn)備好配件后會(huì)通過(guò)一個(gè)開(kāi)頭通知告訴B我已經(jīng)準(zhǔn)備好了,你那邊不用一直等待了,可以繼續(xù)執(zhí)行任務(wù)了。流程A與流程B就是對(duì)應(yīng)的線(xiàn)程A與線(xiàn)程B之間的通信,即可以理解為相互配合,具體也就是“”通知/等待“”機(jī)制!
2、需要注意的細(xì)節(jié)
那么,我們都知道超類(lèi)Object有wait()方法與notify()/notifyAll()方法,在進(jìn)行正式代碼舉例之前,應(yīng)該先加深下對(duì)這三個(gè)方法的理解與一些細(xì)節(jié)(有一些細(xì)節(jié)確實(shí)容易被忽略)
- 調(diào)用wait()方法,會(huì)釋放鎖(這一點(diǎn)我想大部分人都知道),線(xiàn)程狀態(tài)由RUNNING->WAITNG,當(dāng)前線(xiàn)程進(jìn)入對(duì)象等待隊(duì)列中;
- 調(diào)用notify()/notifyAll()方法不會(huì)立馬釋放鎖(這一點(diǎn)我大家人也應(yīng)該知道,但是什么時(shí)候釋放鎖呢?--------請(qǐng)看下一條),notify()方法是將等待隊(duì)列中的線(xiàn)程移到同步隊(duì)列中,而notifyAll()則是全部移到同步隊(duì)列中,被移出的線(xiàn)程狀態(tài)WAITING-->BLOCKED;
- 當(dāng)前調(diào)用notify()/notifyAll()的線(xiàn)程釋放鎖了才算釋放鎖,才有機(jī)會(huì)喚醒wait線(xiàn)程返回(為什么有才有機(jī)會(huì)返回呢?------繼續(xù)看下一條)
- 從wait()返回的前提是必須獲得調(diào)用對(duì)象鎖,也就是說(shuō)notify()與notifyAll()釋放鎖之后,wait()進(jìn)入BLOCKED狀態(tài),如果其他線(xiàn)程有競(jìng)爭(zhēng)當(dāng)前鎖的話(huà),wait線(xiàn)程繼續(xù)爭(zhēng)取鎖資格(不好理解的話(huà),請(qǐng)看下面的代碼舉例)
- 使用wait()、notify()、notifyAll()方法時(shí)需要先調(diào)對(duì)象加鎖(這可能是最容易忽視的點(diǎn)了,至于為什么,請(qǐng)先看了代碼之后,看本篇博文最后補(bǔ)充:wait()、notify()、notifyAll()加鎖的原因----防止線(xiàn)程即饑餓)
二、代碼舉例
1、結(jié)合代碼理解
結(jié)合上述的“工廠流程裝配配件并產(chǎn)出的例子”,我們有兩個(gè)線(xiàn)程(流水線(xiàn))WaitThread與NotifyThread、其中WaitThread是被通知的任務(wù),完成主要的工作(組裝配件完成產(chǎn)品),需要時(shí)刻判斷標(biāo)志位(開(kāi)關(guān));NotifyThread是需要通知的任務(wù),需要對(duì)WaitThread進(jìn)行“監(jiān)督通知”,兩個(gè)配合才能更好完成產(chǎn)品的組裝并輸出。
public class WaitNotify { static Object lock = new Object(); static boolean flag = false; public static void main(String[] args) { new Thread(new WaitThread(), "WaitThread").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new NotifyThread(), "NotifyThread").start(); } /** * 流水線(xiàn)A,完成主要任務(wù) */ static class WaitThread implements Runnable{ @Override public void run() { // 獲取object對(duì)象鎖 synchronized (lock){ // 條件不滿(mǎn)足時(shí)一直在等,等另外的線(xiàn)程改變?cè)摋l件,并通知該wait線(xiàn)程 while (!flag){ try { System.out.println(Thread.currentThread() + " is waiting, flag is "+flag); // wait()方法調(diào)用就會(huì)釋放鎖,當(dāng)前線(xiàn)程進(jìn)入等待隊(duì)列。 lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // TODO 條件已經(jīng)滿(mǎn)足,不繼續(xù)while,完成任務(wù) System.out.println(Thread.currentThread() + " is running, flag is "+flag); } } } /** * 流水線(xiàn)B,對(duì)開(kāi)關(guān)進(jìn)行控制,并通知流水線(xiàn)A */ static class NotifyThread implements Runnable{ @Override public void run() { // 獲取等wait線(xiàn)程同一個(gè)object對(duì)象鎖 synchronized (lock){ flag = true; // 通知wait線(xiàn)程,我已經(jīng)改變了條件,你可以繼續(xù)返回執(zhí)行了(返回之后繼續(xù)判斷while) // 但是此時(shí)通知notify()操作并立即不會(huì)釋放鎖,而是要等當(dāng)前線(xiàn)程釋放鎖 // TODO 我準(zhǔn)備好配件了,我需要通知全部的組裝流水線(xiàn)A..... lock.notifyAll(); System.out.println(Thread.currentThread() + " hold lock, notify waitThread and flag is "+flag); } } } }
運(yùn)行main函數(shù),輸出:
Thread[WaitThread,5,main] is waiting, flag is false
Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true
Thread[WaitThread,5,main] is running, flag is true
車(chē)床流水工作開(kāi)啟,流水線(xiàn)的開(kāi)關(guān)一開(kāi)始是關(guān)閉的(flag=false),流水線(xiàn)B(NotifyThread)去開(kāi)啟后,開(kāi)始自動(dòng)喚醒流水線(xiàn)A(WaitThread),整個(gè)流水線(xiàn)開(kāi)始工作了......
- Thread[WaitThread,5,main] is waiting, flag is false: 一開(kāi)始流水線(xiàn)A發(fā)現(xiàn)自己沒(méi)有配件可租裝,所以等流水線(xiàn)A準(zhǔn)備好配件(這樣是不是覺(jué)得特別傻,哈哈哈,真正的流水線(xiàn)不會(huì)浪費(fèi)時(shí)間等的,而且會(huì)有很多條流水線(xiàn)B準(zhǔn)備配件的,這里只是舉例說(shuō)明,望理解!);
- Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true:流水線(xiàn)B準(zhǔn)備好了配件,開(kāi)啟開(kāi)關(guān)(flag=ture),并通知流水線(xiàn)A,讓流水線(xiàn)A開(kāi)始工作;
- Thread[WaitThread,5,main] is running, flag is true,流水線(xiàn)B收到了通知,再次檢查開(kāi)關(guān)是否開(kāi)啟了,開(kāi)啟的話(huà)就開(kāi)始返回繼續(xù)完成工作了。
其實(shí)結(jié)合上述我舉的例子還是很好理解的,下面是大概的一個(gè)粗略時(shí)序圖:
2、擴(kuò)展理解----wait()返回的前提是獲得了鎖
上述已經(jīng)表達(dá)了這個(gè)注意的細(xì)節(jié):從wait()返回的前提是必須獲得調(diào)用對(duì)象鎖,我們?cè)僭黾幽芨?jìng)爭(zhēng)lock的同步代碼塊(紅字部分)。
public class WaitNotify { static Object lock = new Object(); static boolean flag = false; public static void main(String[] args) { new Thread(new WaitThread(), "WaitThread").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new NotifyThread(), "NotifyThread").start(); } /** * 流水線(xiàn)A,完成主要任務(wù) */ static class WaitThread implements Runnable{ @Override public void run() { // 獲取object對(duì)象鎖 synchronized (lock){ // 條件不滿(mǎn)足時(shí)一直在等,等另外的線(xiàn)程改變?cè)摋l件,并通知該wait線(xiàn)程 while (!flag){ try { System.out.println(Thread.currentThread() + " is waiting, flag is "+flag); // wait()方法調(diào)用就會(huì)釋放鎖,當(dāng)前線(xiàn)程進(jìn)入等待隊(duì)列。 lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // TODO 條件已經(jīng)滿(mǎn)足,不繼續(xù)while,完成任務(wù) System.out.println(Thread.currentThread() + " is running, flag is "+flag); } } } /** * 流水線(xiàn)B,對(duì)開(kāi)關(guān)進(jìn)行控制,并通知流水線(xiàn)A */ static class NotifyThread implements Runnable{ @Override public void run() { // 獲取等wait線(xiàn)程同一個(gè)object對(duì)象鎖 synchronized (lock){ flag = true; // 通知wait線(xiàn)程,我已經(jīng)改變了條件,你可以繼續(xù)返回執(zhí)行了(返回之后繼續(xù)判斷while) // 但是此時(shí)通知notify()操作并立即不會(huì)釋放鎖,而是要等當(dāng)前線(xiàn)程釋放鎖 // TODO 我準(zhǔn)備好配件了,我需要通知全部的組裝流水線(xiàn)A..... lock.notifyAll(); System.out.println(Thread.currentThread() + " hold lock, notify waitThread and flag is "+flag); } // 模擬跟流水線(xiàn)B競(jìng)爭(zhēng) synchronized (lock){ System.out.println(Thread.currentThread() + " hold lock again"); } } } }
輸出結(jié)果:
Thread[WaitThread,5,main] is waiting, flag is false
Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true
Thread[NotifyThread,5,main] hold lock again
Thread[WaitThread,5,main] is running, flag is true
其中第三條跟第四條順序可能會(huì)反著來(lái)的,這就是因?yàn)閘ock鎖可能被紅字部分的synchronized代碼塊競(jìng)爭(zhēng)獲?。ㄟ@樣wait()方法可能獲取不到lock鎖,不會(huì)返回),也可能被waitThread獲取從wait()方法返回。
Thread[WaitThread,5,main] is waiting, flag is false
Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true
Thread[WaitThread,5,main] is running, flag is true
Thread[NotifyThread,5,main] hold lock again
三、等待/通知模式的應(yīng)用
1、Thread.join()中源碼應(yīng)用
Thread.join()作用:當(dāng)線(xiàn)程A等待thread線(xiàn)程終止之后才從thread.join()返回, 每個(gè)線(xiàn)程終止的前提是前驅(qū)線(xiàn)程終止,每個(gè)線(xiàn)程等待前驅(qū)線(xiàn)程終止后,才從join方法返回,這里涉及了等待/通知機(jī)制(等待前驅(qū)線(xiàn)程結(jié)束,接收前驅(qū)線(xiàn)程結(jié)束通知)。
Thread.join()源碼中,使用while選好判斷前驅(qū)線(xiàn)程是否活著,如果前驅(qū)線(xiàn)程還活著就一直wait等待,當(dāng)然如果超時(shí)的話(huà)就直接返回。
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } // 這里的while(){wait(millis)} 就是利用等待/通知中的等待模式,只不過(guò)加上了超時(shí)設(shè)置 if (millis == 0) { // while循環(huán),當(dāng)線(xiàn)程還活著的時(shí)候就一直循環(huán)等待,直到線(xiàn)程終止 while (isAlive()) { // wait等待 wait(0); } // 條件滿(mǎn)足時(shí)返回 } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
2、其它的應(yīng)用
線(xiàn)程池的本質(zhì)是使用一個(gè)線(xiàn)程安全的工作隊(duì)列連接工作者線(xiàn)程和客戶(hù)端線(xiàn)程,客戶(hù)端線(xiàn)程將任務(wù)放入工作隊(duì)列后便返回,而工作者線(xiàn)程則不斷地從工作隊(duì)列中取出工作并執(zhí)行。那么,在這里的等待/通知模式的應(yīng)用就是:
工作隊(duì)列中線(xiàn)程job沒(méi)有的話(huà)也就是工作隊(duì)列為空的情況下,等待客戶(hù)端放入工作隊(duì)列線(xiàn)程任務(wù),并通知工作線(xiàn)程繼續(xù)從工作隊(duì)列中獲取線(xiàn)程執(zhí)行。
注:關(guān)于線(xiàn)程池的應(yīng)用源碼這里不做介紹,因?yàn)橐粫r(shí)也講不完(自己也還沒(méi)有完全消化),先簡(jiǎn)單介紹下應(yīng)用到的地方還有概念。
補(bǔ)充:其實(shí)數(shù)據(jù)庫(kù)的連接池也類(lèi)似線(xiàn)程池這種工作流程,也會(huì)涉及等待/通知模式。
3、等待/通知范式
介紹了那么多應(yīng)用,這種模式應(yīng)該有個(gè)統(tǒng)一的范式來(lái)套用。對(duì)的,必然是有的:
對(duì)于等待者(也可以稱(chēng)之為消費(fèi)者):
synchronized (對(duì)象lock) { while (條件不滿(mǎn)足) { 對(duì)象.wait(); } // TODO 處理邏輯 }
對(duì)于通知者(也可以稱(chēng)之為生產(chǎn)者):
synchronized (對(duì)象lock) { while (條件滿(mǎn)足) { 改變條件 對(duì)象.notify(); } }
注意:實(shí)際開(kāi)發(fā)中最好采用的是超時(shí)等待/通知模式,在thread.join()源碼方法中完美體現(xiàn)
四、wait()、notify()、notifyAll()使用前需要加鎖的原因----防止線(xiàn)程即饑餓
(1)其實(shí)根據(jù)wait()注意事項(xiàng)也能明白,wait()是釋放鎖的,那么不加鎖哪來(lái)釋放鎖!
(2)wait()與notify()或者notifyAll()必須是搭配一起使用的,否則線(xiàn)程調(diào)用object.wait()之后,沒(méi)有超時(shí)機(jī)制,也沒(méi)有調(diào)用notify()或者notifyAll()喚醒的話(huà),就一直處于WAITING狀態(tài),造成調(diào)用wait()的線(xiàn)程一直都是饑餓狀態(tài)。
(3)由于第2條的,我們已知:即便我們使用了notify()或者notifyAll()去喚醒線(xiàn)程,但是沒(méi)有在適當(dāng)?shù)臅r(shí)機(jī)喚醒(比如調(diào)用wait()之前就喚醒了),那么仍然調(diào)用wait()線(xiàn)程處于WAITING狀態(tài),所以我們必須保證wait()方法要么不執(zhí)行,要么就執(zhí)行完在被喚醒。也就是下列代碼中1那里不能允許插入調(diào)用notify/notifyAll,自然而然就增加synchronized關(guān)鍵字,保證wait()操作整體執(zhí)行不被破壞!
synchronized (對(duì)象lock) { while (條件不滿(mǎn)足) { // 1 這里如果先執(zhí)行了notify/notifyAll方法,那么2執(zhí)行之后,該線(xiàn)程就一直WAITING 對(duì)象.wait(); // 2 } // TODO 處理邏輯 }
用圖片展示執(zhí)行順序就是:
(4)注意synchronized代碼塊中,代碼錯(cuò)誤或者其它原因線(xiàn)程終止的話(huà),沒(méi)有執(zhí)行到wait()方法的話(huà),是會(huì)自動(dòng)釋放鎖的,不必?fù)?dān)心會(huì)死鎖。
到此這篇關(guān)于Java多線(xiàn)程中wait/notify通信模式的文章就介紹到這了,更多相關(guān)Java多線(xiàn)程wait/notify通信模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中整合knife4j接口文檔的實(shí)踐
這篇文章主要介紹了SpringBoot中整合knife4j接口文檔的實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09關(guān)于request.getRequestDispatcher().forward()的妙用及DispatcherType
這篇文章主要介紹了關(guān)于request.getRequestDispatcher().forward()的妙用及DispatcherType對(duì)Filter配置的影響,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01解決IDEA項(xiàng)目project包目錄消失的問(wèn)題
這篇文章主要介紹了解決IDEA項(xiàng)目project包目錄消失的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02用intellij Idea加載eclipse的maven項(xiàng)目全流程(圖文)
這篇文章主要介紹了用intellij Idea加載eclipse的maven項(xiàng)目全流程(圖文),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12rabbitmq結(jié)合spring實(shí)現(xiàn)消息隊(duì)列優(yōu)先級(jí)的方法
本篇文章主要介紹了rabbitmq結(jié)合spring實(shí)現(xiàn)消息隊(duì)列優(yōu)先級(jí)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02Spring中的模塊與應(yīng)用場(chǎng)景詳解
這篇文章主要介紹了Spring中的模塊與應(yīng)用場(chǎng)景詳解,Spring 框架可以為 Java 應(yīng)用程序開(kāi)發(fā)提供全面的基礎(chǔ)設(shè)施支持,它是現(xiàn)在非常流行的 Java 開(kāi)源框架,對(duì)于一個(gè) Java 開(kāi)發(fā)人員來(lái)說(shuō),熟練掌握 Spring 是必不可少的,需要的朋友可以參考下2023-09-09SpringBoot整合Scala構(gòu)建Web服務(wù)的方法
這篇文章主要介紹了SpringBoot整合Scala構(gòu)建Web服務(wù)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03