1. 程式人生 > >超強圖文|併發程式設計【等待/通知機制】就是這個feel~

超強圖文|併發程式設計【等待/通知機制】就是這個feel~

![](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074941613-1489223916.png) > - 你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想 > > - If you can NOT explain it simply, you do NOT understand it well enough 現陸續將Demo程式碼和技術文章整理在一起 [Github實踐精選](https://github.com/FraserYu/learnings) ,方便大家閱讀檢視,本文同樣收錄在此,覺得不錯,還請Star ![](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074943050-752187530.png) ## 併發程式設計為什麼會有等待通知機制 上一篇文章說明了 [Java併發死鎖解決思路](https://dayarch.top/p/java-concurrency-dead-lock.html) , 解決死鎖的思路之一就是 `破壞請求和保持條件`, 所有櫃員都要通過**唯一**的賬本管理員一次性拿到所有轉賬業務需要的賬本,就像下面這樣: ![](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074943496-1802163601.png) 沒有等待/通知機制之前,所有櫃員都通過死迴圈的方式不斷向賬本管理員申請所有賬本,程式的體現就是這樣: ```java while(!accountBookManager.getAllRequiredAccountBook(this, target)) ; ``` 假如賬本管理員是年輕小夥,腿腳利落(即執行 getAllRequiredAccountBook方法耗時短),並且多個櫃員轉賬的業務衝突量不大,這個方案簡單粗暴且有效,櫃員只需要嘗試幾次就可以成功(即通過少量的迴圈可以實現) 過了好多年,年輕的賬本管理員變成了年邁的老人,行動遲緩(即執行 getAllRequiredAccountBook 耗時長),同時,多個櫃員轉賬的業務衝突量也變大,之前幾十次迴圈能做到的,現在可能就要申請成千上百,甚至上萬次才能完成一次轉賬 ![](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074944062-1756663620.png) **人工無限申請浪費口舌, 程式無限申請浪費CPU。聰明的人就想到了 `等待/通知` 機制** ## 等待/通知機制 無限迴圈實在太浪費CPU,而理想情況應該是這樣: - 櫃員A如果拿不到所有賬本,就傲嬌的不再繼續問了(執行緒阻塞自己 wait) - 櫃員B歸還了櫃員A需要的賬本之後就主動通知櫃員A賬本可用(通知等待的執行緒 notify/notifyAll) 做到這樣,就能避免迴圈等待消耗CPU的問題了 --- 現實中有太多場景都在應用等待/通知機制。歡迎觀臨紅浪漫,比如去XX辦證,去醫院就醫/體檢。 下面請自行腦補一下去醫院就醫或體檢的畫面, 整體流程類似這樣: | 序號 | 就醫 | 程式解釋(自己的視角) | | ---- | :--------------------------------------------------- | ------------------------------------------------ | | 1 | 掛號成功,到診室門口排號候診 | 排號的患者(執行緒)嘗試獲取【互斥鎖】 | | 2 | 大夫叫到自己,進入診室就診 | 自己【獲取到互斥鎖】 | | 3 | 大夫簡單詢問,要求做檢查(患者缺乏報告不能診斷病因) | 進行【條件判斷】,執行緒要求的條件【沒滿足】 | | 4 | 自己出去做檢查 | 執行緒【主動釋放】持有的互斥鎖 | | 5 | 大夫叫下一位患者 | 另一位患者(執行緒)獲取到互斥鎖 | | 6 | 自己拿到檢測報告 | 執行緒【曾經】要求的條件得到滿足(實則【被通知】) | | 7 | 再次在診室門口排號候診 | 再次嘗試獲取互斥鎖 | | 8 | ... | ... | 在【程式解釋】一列,我將關鍵字(排隊、鎖、等待、釋放....)已經用 `【】` 框了起來。Java 語言中,其內建的關鍵字 `synchronized` 和 方法`wait(),notify()/notifyAll()` 就能實現上面提到的等待/通知機制,我們將這幾個關鍵字實現流程現形象化的表示一下: ![等待佇列圖](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074945404-535856850.png) 這可不是一個簡單的圖,下面還要圍繞這個圖做很多文章,不過這裡我必須要插播幾個面試基礎知識點了: 1. 一個鎖對應一個【入口等待佇列】,不同鎖的入口等待佇列沒任何關係,說白了他們就不存在競爭關係。你想呀,不同患者進入眼科和耳鼻喉科看大夫一點衝突都沒有 2. `wait(), notify()/notifyAll()` 要在 synchronized 內部被使用,並且,如果鎖的物件是this,就要 `this.wait(),this.notify()/this.notifyAll()` , 否則JVM就會丟擲 `java.lang.IllegalMonitorStateException` 的。你想呀,等待/通知機制就是從【競爭】環境逐漸衍生出來的策略,不在鎖競爭內部使用或等待/通知錯了物件, 自然是不符合常理的 ![](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074945949-1712471555.png) 有了上面知識的鋪墊,要想將無限迴圈策略改為等待通知策略,你還需要問自己四個問題: ### 靈魂 4 問 ![](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074946769-76129469.png) 我們拿錢莊賬本管理員的例子依依做以上回答: ![](https://img2020.cnblogs.com/other/1583165/202003/1583165-20200317074948385-92719355.png) 我們優化錢莊轉賬的程式: ```java public class AccountBookManager