為什么wait和notify必須放在synchronized中使用
前言:
在多線程編程中,wait 方法是讓當(dāng)前線程進(jìn)入休眠狀態(tài),直到另一個(gè)線程調(diào)用了 notify 或 notifyAll 方法之后,才能繼續(xù)恢復(fù)執(zhí)行。而在 Java 中,wait 和 notify/notifyAll 有著一套自己的使用格式要求,也就是在使用 wait 和 notify(notifyAll 的使用和 notify 類似,所以下文就只用 notify 用來指代二者)必須配合 synchronized 一起使用才行。
wait/notify基礎(chǔ)使用
wait 和 notify 的基礎(chǔ)方法如下:
Object lock = new Object(); new Thread(() -> { synchronized (lock) { try { System.out.println("wait 之前"); // 調(diào)用 wait 方法 lock.wait(); System.out.println("wait 之后"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); Thread.sleep(100); synchronized (lock) { System.out.println("執(zhí)行 notify"); // 調(diào)用 notify 方法 lock.notify(); }
以上代碼的執(zhí)行結(jié)果如下圖所示:
wait/notify和synchronized一起用?
那問題來了,是不是 wait 和 notify 一定要配合 synchronized 一起使用呢?wait 和 notify 單獨(dú)使用行不行呢? 我們嘗試將以上代碼中的 synchronized 代碼行刪除,實(shí)現(xiàn)代碼如下:
初看代碼好像沒啥問題,編譯器也沒報(bào)錯(cuò),好像能“正常使用”,然而當(dāng)我們運(yùn)行以上程序時(shí)就會發(fā)生如下錯(cuò)誤:
從上述結(jié)果可以看出:無論是 wait 還是 notify,如果不配合 synchronized 一起使用,在程序運(yùn)行時(shí)就會報(bào) IllegalMonitorStateException 非法的監(jiān)視器狀態(tài)異常,而且 notify 也不能實(shí)現(xiàn)程序的喚醒功能了。
原因分析
從上述的報(bào)錯(cuò)信息我們可以看出,JVM 在運(yùn)行時(shí)會強(qiáng)制檢查 wait 和 notify 有沒有在 synchronized 代碼中,如果沒有的話就會報(bào)非法監(jiān)視器狀態(tài)異常(IllegalMonitorStateException),但這也僅僅是運(yùn)行時(shí)的程序表象,那為什么 Java 要這樣設(shè)計(jì)呢? 其實(shí)這樣設(shè)計(jì)的原因就是為了防止多線程并發(fā)運(yùn)行時(shí),程序的執(zhí)行混亂問題。初看這句話,好像是用來描述“鎖”的。然而實(shí)際情況也是如此,wait 和 notify 引入鎖就是來規(guī)避并發(fā)執(zhí)行時(shí)程序的執(zhí)行混亂問題的。那這個(gè)“執(zhí)行混亂問題”到底是啥呢?接下來我們繼續(xù)往下看。
wait和notify問題復(fù)現(xiàn)
我們假設(shè) wait 和 notify 可以不加鎖,我們用它們來實(shí)現(xiàn)一個(gè)自定義阻塞隊(duì)列。 這里的阻塞隊(duì)列是指讀操作阻塞,也就是當(dāng)讀取數(shù)據(jù)時(shí),如果有數(shù)據(jù)就返回?cái)?shù)據(jù),如果沒有數(shù)據(jù)則阻塞等待數(shù)據(jù),實(shí)現(xiàn)代碼如下:
class MyBlockingQueue { // 用來保存數(shù)據(jù)的集合 Queue<String> queue = new LinkedList<>(); /** * 添加方法 */ public void put(String data) { // 隊(duì)列加入數(shù)據(jù) queue.add(data); // 喚醒線程繼續(xù)執(zhí)行(這里的線程指的是執(zhí)行 take 方法的線程) notify(); // ③ } /** * 獲取方法(阻塞式執(zhí)行) * 如果隊(duì)列里面有數(shù)據(jù)則返回?cái)?shù)據(jù),如果沒有數(shù)據(jù)就阻塞等待數(shù)據(jù) * @return */ public String take() throws InterruptedException { // 使用 while 判斷是否有數(shù)據(jù)(這里使用 while 而非 if 是為了防止虛假喚醒) while (queue.isEmpty()) { // ① // 沒有任務(wù),先阻塞等待 wait(); // ② } return queue.remove(); // 返回?cái)?shù)據(jù) } }
注意上述代碼,我們在代碼中標(biāo)識了三個(gè)關(guān)鍵執(zhí)行步驟:①:判斷隊(duì)列中是否有數(shù)據(jù);②:執(zhí)行 wait 休眠操作;③:給隊(duì)列中添加數(shù)據(jù)并喚醒阻塞線程。 如果不強(qiáng)制要求添加 synchronized,那么就會出現(xiàn)如下問題:
步驟 | 線程1 | 線程2 |
---|---|---|
1 | 執(zhí)行步驟 ① 判斷當(dāng)前隊(duì)列中沒有數(shù)據(jù) | |
2 | 執(zhí)行步驟 ③ 將數(shù)據(jù)添加到隊(duì)列,并喚醒線程1繼續(xù)執(zhí)行 | |
3 | 執(zhí)行步驟 ② 線程 1 進(jìn)入休眠狀態(tài) |
從上述執(zhí)行流程看出問題了嗎? 如果 wait 和 notify 不強(qiáng)制要求加鎖,那么在線程 1 執(zhí)行完判斷之后,尚未執(zhí)行休眠之前,此時(shí)另一個(gè)線程添加數(shù)據(jù)到隊(duì)列中。然而這時(shí)線程 1 已經(jīng)執(zhí)行過判斷了,所以就會直接進(jìn)入休眠狀態(tài),從而導(dǎo)致隊(duì)列中的那條數(shù)據(jù)永久性不能被讀取,這就是程序并發(fā)運(yùn)行時(shí)“執(zhí)行結(jié)果混亂”的問題。 然而如果配合 synchronized 一起使用的話,代碼就會變成以下這樣:
class MyBlockingQueue { // 用來保存任務(wù)的集合 Queue<String> queue = new LinkedList<>(); /** * 添加方法 */ public void put(String data) { synchronized (MyBlockingQueue.class) { // 隊(duì)列加入數(shù)據(jù) queue.add(data); // 為了防止 take 方法阻塞休眠,這里需要調(diào)用喚醒方法 notify notify(); // ③ } } /** * 獲取方法(阻塞式執(zhí)行) * 如果隊(duì)列里面有數(shù)據(jù)則返回?cái)?shù)據(jù),如果沒有數(shù)據(jù)就阻塞等待數(shù)據(jù) * @return */ public String take() throws InterruptedException { synchronized (MyBlockingQueue.class) { // 使用 while 判斷是否有數(shù)據(jù)(這里使用 while 而非 if 是為了防止虛假喚醒) while (queue.isEmpty()) { // ① // 沒有任務(wù),先阻塞等待 wait(); // ② } } return queue.remove(); // 返回?cái)?shù)據(jù) } }
這樣改造之后,關(guān)鍵步驟 ① 和關(guān)鍵步驟 ② 就可以一起執(zhí)行了,從而當(dāng)線程執(zhí)行了步驟 ③ 之后,線程 1 就可以讀取到隊(duì)列中的那條數(shù)據(jù)了,它們的執(zhí)行流程如下:
步驟 | 線程1 | 線程2 |
---|---|---|
1 | 執(zhí)行步驟 ① 判斷當(dāng)前隊(duì)列沒有數(shù)據(jù) | |
2 | 執(zhí)行步驟 ② 線程進(jìn)入休眠狀態(tài) | |
3 | 執(zhí)行步驟 ③ 將數(shù)據(jù)添加到隊(duì)列,并執(zhí)行喚醒操作 | |
4 | 線程被喚醒,繼續(xù)執(zhí)行 | |
5 | 判斷隊(duì)列中有數(shù)據(jù),返回?cái)?shù)據(jù) |
這樣咱們的程序就可以正常執(zhí)行了,這就是為什么 Java 設(shè)計(jì)一定要讓 wait 和 notify 配合上 synchronized 一起使用的原因了。
總結(jié)
本文介紹了 wait 和 notify 的基礎(chǔ)使用,以及為什么 wait 和 notify/notifyAll 一定要配合 synchronized 使用的原因。如果 wait 和 notify/notifyAll 不強(qiáng)制和 synchronized 一起使用,那么在多線程執(zhí)行時(shí),就會出現(xiàn) wait 執(zhí)行了一半,然后又執(zhí)行了添加數(shù)據(jù)和 notify 的操作,從而導(dǎo)致線程一直休眠的缺陷。?
到此這篇關(guān)于為什么wait和notify必須放在synchronized中使用的文章就介紹到這了,更多相關(guān)wait與notify的使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java System類詳解_動力節(jié)點(diǎn)Java學(xué)院整理
System類是jdk提供的一個(gè)工具類,有final修飾,不可繼承,由名字可以看出來,其中的操作多數(shù)和系統(tǒng)相關(guān)。這篇文章主要介紹了Java System類詳解_動力節(jié)點(diǎn)Java學(xué)院整理,需要的朋友可以參考下2017-04-04mybatis?<foreach>標(biāo)簽動態(tài)增刪改查方式
這篇文章主要介紹了mybatis?<foreach>標(biāo)簽動態(tài)增刪改查方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03Springmvc DispatcherServlet原理及用法解析
這篇文章主要介紹了Springmvc DispatcherServlet原理及用法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09關(guān)于SpringBoot自定義條件注解與自動配置
這篇文章主要介紹了關(guān)于SpringBoot自定義條件注解與自動配置,Spring Boot的核心功能就是為整合第三方框架提供自動配置,而本文則帶著大家實(shí)現(xiàn)了自己的自動配置和Starter,需要的朋友可以參考下2023-07-07