為什么wait/notify必須要和synchronized一起使用?

作者 | 磊哥
來源 | Java面試真題解析(ID:aimianshi666)
轉載請聯(lián)系授權(微信ID:GG_Stone)
在多線程編程中,wait 方法是讓當前線程進入休眠狀態(tài),直到另一個線程調(diào)用了 notify 或 notifyAll 方法之后,才能繼續(xù)恢復執(zhí)行。而在 Java 中,wait 和 notify/notifyAll 有著一套自己的使用格式要求,也就是在使用 wait 和 notify(notifyAll 的使用和 notify 類似,所以下文就只用 notify 用來指代二者)必須配合 synchronized 一起使用才行。
wait/notify基礎使用
wait 和 notify 的基礎方法如下:
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í)行結果如下圖所示:
wait/notify和synchronized一起用?
那問題來了,是不是 wait 和 notify 一定要配合 synchronized 一起使用呢?wait 和 notify 單獨使用行不行呢?我們嘗試將以上代碼中的 synchronized 代碼行刪除,實現(xiàn)代碼如下:
初看代碼好像沒啥問題,編譯器也沒報錯,好像能“正常使用”,然而當我們運行以上程序時就會發(fā)生如下錯誤:
從上述結果可以看出:無論是 wait 還是 notify,如果不配合 synchronized 一起使用,在程序運行時就會報 IllegalMonitorStateException 非法的監(jiān)視器狀態(tài)異常,而且 notify 也不能實現(xiàn)程序的喚醒功能了。
原因分析
從上述的報錯信息我們可以看出,JVM 在運行時會強制檢查 wait 和 notify 有沒有在 synchronized 代碼中,如果沒有的話就會報非法監(jiān)視器狀態(tài)異常(IllegalMonitorStateException),但這也僅僅是運行時的程序表象,那為什么 Java 要這樣設計呢?其實這樣設計的原因就是為了防止多線程并發(fā)運行時,程序的執(zhí)行混亂問題。初看這句話,好像是用來描述“鎖”的。然而實際情況也是如此,wait 和 notify 引入鎖就是來規(guī)避并發(fā)執(zhí)行時程序的執(zhí)行混亂問題的。那這個“執(zhí)行混亂問題”到底是啥呢?接下來我們繼續(xù)往下看。
wait和notify問題復現(xiàn)
我們假設 wait 和 notify 可以不加鎖,我們用它們來實現(xiàn)一個自定義阻塞隊列。這里的阻塞隊列是指讀操作阻塞,也就是當讀取數(shù)據(jù)時,如果有數(shù)據(jù)就返回數(shù)據(jù),如果沒有數(shù)據(jù)則阻塞等待數(shù)據(jù),實現(xiàn)代碼如下:
class?MyBlockingQueue?{
????//?用來保存數(shù)據(jù)的集合
????Queue?queue?=?new?LinkedList<>();
????/**
?????*?添加方法
?????*/
????public?void?put(String?data)?{
????????//?隊列加入數(shù)據(jù)
????????queue.add(data);?
????????//?喚醒線程繼續(xù)執(zhí)行(這里的線程指的是執(zhí)行?take?方法的線程)
????????notify();?//?③
????}
????/**
?????*?獲取方法(阻塞式執(zhí)行)
?????*?如果隊列里面有數(shù)據(jù)則返回數(shù)據(jù),如果沒有數(shù)據(jù)就阻塞等待數(shù)據(jù)
?????*?@return
?????*/
????public?String?take()?throws?InterruptedException?{
????????//?使用?while?判斷是否有數(shù)據(jù)(這里使用?while?而非?if?是為了防止虛假喚醒)
????????while?(queue.isEmpty())?{?//?①??
????????????//?沒有任務,先阻塞等待
????????????wait();?//?②
????????}
????????return?queue.remove();?//?返回數(shù)據(jù)
????}
}
注意上述代碼,我們在代碼中標識了三個關鍵執(zhí)行步驟:①:判斷隊列中是否有數(shù)據(jù);②:執(zhí)行 wait 休眠操作;③:給隊列中添加數(shù)據(jù)并喚醒阻塞線程。如果不強制要求添加 synchronized,那么就會出現(xiàn)如下問題:
| 步驟 | 線程1 | 線程2 |
|---|---|---|
| 1 | 執(zhí)行步驟 ① 判斷當前隊列中沒有數(shù)據(jù) | |
| 2 | 執(zhí)行步驟 ③ 將數(shù)據(jù)添加到隊列,并喚醒線程1繼續(xù)執(zhí)行 | |
| 3 | 執(zhí)行步驟 ② 線程 1 進入休眠狀態(tài) |
從上述執(zhí)行流程看出問題了嗎?如果 wait 和 notify 不強制要求加鎖,那么在線程 1 執(zhí)行完判斷之后,尚未執(zhí)行休眠之前,此時另一個線程添加數(shù)據(jù)到隊列中。然而這時線程 1 已經(jīng)執(zhí)行過判斷了,所以就會直接進入休眠狀態(tài),從而導致隊列中的那條數(shù)據(jù)永久性不能被讀取,這就是程序并發(fā)運行時“執(zhí)行結果混亂”的問題。然而如果配合 synchronized 一起使用的話,代碼就會變成以下這樣:
class?MyBlockingQueue?{
????//?用來保存任務的集合
????Queue?queue?=?new?LinkedList<>();
????/**
?????*?添加方法
?????*/
????public?void?put(String?data)?{
????????synchronized?(MyBlockingQueue.class)?{
????????????//?隊列加入數(shù)據(jù)
????????????queue.add(data);
????????????//?為了防止?take?方法阻塞休眠,這里需要調(diào)用喚醒方法?notify
????????????notify();?//?③
????????}
????}
????/**
?????*?獲取方法(阻塞式執(zhí)行)
?????*?如果隊列里面有數(shù)據(jù)則返回數(shù)據(jù),如果沒有數(shù)據(jù)就阻塞等待數(shù)據(jù)
?????*?@return
?????*/
????public?String?take()?throws?InterruptedException?{
????????synchronized?(MyBlockingQueue.class)?{
????????????//?使用?while?判斷是否有數(shù)據(jù)(這里使用?while?而非?if?是為了防止虛假喚醒)
????????????while?(queue.isEmpty())?{??//?①
????????????????//?沒有任務,先阻塞等待
????????????????wait();?//?②
????????????}
????????}
????????return?queue.remove();?//?返回數(shù)據(jù)
????}
}
這樣改造之后,關鍵步驟 ① 和關鍵步驟 ② 就可以一起執(zhí)行了,從而當線程執(zhí)行了步驟 ③ 之后,線程 1 就可以讀取到隊列中的那條數(shù)據(jù)了,它們的執(zhí)行流程如下:
| 步驟 | 線程1 | 線程2 |
|---|---|---|
| 1 | 執(zhí)行步驟 ① 判斷當前隊列沒有數(shù)據(jù) | |
| 2 | 執(zhí)行步驟 ② 線程進入休眠狀態(tài) | |
| 3 | 執(zhí)行步驟 ③ 將數(shù)據(jù)添加到隊列,并執(zhí)行喚醒操作 | |
| 4 | 線程被喚醒,繼續(xù)執(zhí)行 | |
| 5 | 判斷隊列中有數(shù)據(jù),返回數(shù)據(jù) |
這樣咱們的程序就可以正常執(zhí)行了,這就是為什么 Java 設計一定要讓 wait 和 notify 配合上 synchronized 一起使用的原因了。
總結
本文介紹了 wait 和 notify 的基礎使用,以及為什么 wait 和 notify/notifyAll 一定要配合 synchronized 使用的原因。如果 wait 和 notify/notifyAll 不強制和 synchronized 一起使用,那么在多線程執(zhí)行時,就會出現(xiàn) wait 執(zhí)行了一半,然后又執(zhí)行了添加數(shù)據(jù)和 notify 的操作,從而導致線程一直休眠的缺陷。
是非審之于己,毀譽聽之于人,得失安之于數(shù)。
公眾號:Java面試真題解析
面試合集:https://gitee.com/mydb/interview

面試突擊23:說一下線程生命周期,以及轉換過程?
面試突擊22:為什么start方法不能重復調(diào)用?而run方法卻可以?
有哪些創(chuàng)建線程的方法?推薦使用哪種?
求點贊、在看、分享三連




