在鎖與監視器中有對wait和notify以及notifyAll進行了簡單介紹
所有對象都有一個與之關聯的鎖與監視器
wait和notify以及notifyAll之所以是Object的方法就是因為任何一個對象都可以當做鎖對象(鎖對象也是一種臨界資源)
而等待與喚醒本身就是指的臨界資源
- 等待,等待什么?等待獲取臨界資源
- 喚醒,喚醒什么?喚醒等待臨界資源的線程
所以說,等也好,喚醒也罷,都離不開臨界資源,而那個作為鎖的Object,就是臨界資源
這也是為什么必須在同步方法(同步代碼塊)中使用wait和notify、notifyAll,因為他們必須持有臨界資源(鎖)的監視器,只有持有了指定鎖的監視器,才能夠進行相關操作,而且,必須是持有的哪個鎖,才能夠在這個鎖(臨界資源)上進行操作
這個也很容易接受與理解,因為線程的通信在Java中是針對監視器(鎖、臨界資源)的,在監視器上的等待與喚醒
你都沒持有監視器,你還搞什么?你持有的A監視器,你在B監視器上搞什么?
線程通信
wait與notify示例
下面的代碼示例中,MessageQueue類,有內部有LinkedList,可以用於保存消息,消息為Message
MessageQueue內部個數默認10,可以通過構造函數進行手動設置
提供了生產方法set和獲取方法get
如果隊列已滿,等待,否則生產消息,並且通知消費者獲取消息
如果隊列已空,等待,否則消費消息,並且通知生產者生產消息
在測試類中開辟兩個線程,一個用於生產,一個用於消費(無限循環執行)
package test1; import java.util.LinkedList; /** * 消息隊列MessageQueue 測試 */ public class T13 { public static void main(String[] args) { final MessageQueue mq = new MessageQueue(3); System.out.println("***************task begin***************"); //創建生產者線程並啟動 new Thread(() -> { while (true) { mq.set(new Message()); } }, "producer").start(); //創建消費者線程並啟動 new Thread(() -> { while (true) { mq.get(); } }, "consumer").start(); } } /** * 消息隊列 */ class MessageQueue { /** * 隊列最大值 */ private final int max; /* * 鎖 * */ private final byte[] lock = new byte[1]; /** * final確保發布安全 */ final LinkedList<Message> messageQueue = new LinkedList<>(); /** * 構造函數默認隊列大小為10 */ public MessageQueue() { max = 10; } /** * 構造函數設置隊列大小 */ public MessageQueue(int x) { max = x; } public void set(Message message) { synchronized (lock) { //如果已經大於隊列個數,隊列滿,進入等待 if (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊列未滿,生產消息,隨后通知lock上的等待線程 //每一次的消息生產,都會通知消費者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); lock.notify(); } } public void get() { synchronized (lock) { //如果隊列為空,進入等待,無法獲取消息 if (messageQueue.isEmpty()) { try { System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //隊列非空時,讀取消息,隨后通知lock上的等待線程 //每一次的消息讀取,都會通知生產者 System.out.println(Thread.currentThread().getName() + " : get a message"); messageQueue.removeFirst(); lock.notify(); } } } /** * 消息隊列中存儲的消息 */ class Message { }
ps:判斷條件 if (messageQueue.size() > max) 所以實際隊列空間為4
從以上代碼示例中可以看得出來,借助於鎖lock,實現了生產者和消費者之間的通信與互斥
他們都是基於這個臨界資源進行管理的,這個鎖就相當於調度的中心,進入了監視器之后如果條件滿足,那么執行,並且會通知其他線程,如果不滿足則會等待。
從這個例子中應該可以理解,鎖與監視器 和 線程通信之間的關系
wait方法
有三個版本的wait方法,wait,表示在等待此鎖(等待持有這個鎖對象對應的監視器)
對於無參數的wait以及雙參數的wait,可以查看源代碼,核心為這個native方法
wait()直接調用wait(0);
wait(long timeout, int nanos)在參數有效性校驗后調用wait(timeout)
深入看下native方法
API解釋:
在其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量前,導致當前線程等待。
如前面所述,wait以及notify以及notifyAll都需要持有監視器才可以調用該方法
既然另外兩個版本都是依賴底層的這個wait,所以所有版本的wait都需要持有監視器
一旦該方法調用,將會進入該監視器的等待集,並且放棄同步要求(也就是不再持有鎖,將會釋放鎖)
一定注意:將會釋放鎖,將會釋放鎖,會釋放鎖......
除非遇到上面的這幾種情況,否則將會線程被禁用,進入休眠狀態,也就是持續等待
遇到這幾種情況后,將會從對象的等待集中刪除線程,並重新進行線程調度
需要注意的是從等待集中刪除並不意味着立馬執行,他仍舊需要與其他線程競爭,如果競爭失敗,也會繼續等待
如果一個線程在不止一個鎖對象的等待集內,那么將只是解除當前這個鎖對象等待集中解鎖,在其他等待集中仍舊是鎖定的,如果你在多個等待集合中,總不能一下子就從所有的等待集合中釋放,對吧
如果在等待時,任何其他的線程中斷了該線程,那么將會收到一個異常,InterruptedException
另外如果沒有持有當前監視器,將會拋出異常,IllegalMonitorStateException
小結:
對於native方法wait,將會等待指定的時長,如果wait(0),將會持續等待
無參數的wait()就是持續等待
雙參數版本的就是等待一定的時長
wait的虛假喚醒
在沒有被通知、中斷或超時的情況下,線程也可能被喚醒,這被稱之為虛假喚醒 (spurious wakeup)
也就是說你沒有讓他醒來(通知、中斷、超時),這完全是超出你意料的,自己就莫名的醒了
盡管這種事情發生的概率很小,但是還是應該注意防范
如何防范?
比如我們上面的生產者方法
public void set(Message message) { synchronized (lock) { //如果已經大於隊列個數,隊列滿,進入等待 if (messageQueue.size() > max) { try { System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting..."); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊列未滿,生產消息,隨后通知lock上的等待線程 //每一次的消息生產,都會通知消費者 System.out.println(Thread.currentThread().getName() + " : add a message"); messageQueue.addLast(message); lock.notify(); } }
生產者方法中,我們使用if對條件進行判斷
if (messageQueue.size() > max)
一旦出現虛假喚醒,那么將會從wait方法后面繼續執行,也就是下面的
messageQueue.addLast(message);
lock.notify();
很顯然,虛假喚醒的時候,條件很可能是仍舊不滿足的,繼續生產,豈不出錯?
所以我們應該喚醒后再次的進行條件判斷,如何進行?
可以把if條件判斷換成while條件測試,這樣即使喚醒了也會再次的確認是否條件滿足,如果不滿足那么肯定會繼續進入等待,而不會繼續往下執行
小結:
我們應該總是使用循環測試條件來確保條件的確滿足,避免小概率發生的虛假喚醒問題
notify方法
notify也是一個本地方法,他將會喚醒在該監視器上等待的某個線程(關鍵詞:當前監視器、某一個線程)
即使在該監視器上有多個線程正在等待,那么也是僅僅喚醒一個
而且,選擇是任意的
另外還需要注意,是這邊notify之后,那么立刻就有什么反應了嗎?不是的!
只有當前持有監視器的線程執行結束,才有機會執行被喚醒的線程,而且被喚醒的線程仍舊需要參與競爭(如果入口集中還有線程在等待的話)
所以,如果一個1000行的方法,不管你在哪一行執行notify,終歸是要方法結束后,被喚醒的線程才有機會
notify問題
notify僅僅喚醒其中一個線程,而且,這種機制是非公平的,也就是說不能夠保障每個線程必然都有機會獲得執行。
換個說法,比如10個小朋友等待老師發糖果,如果每次都隨機選一個,可能有的小朋友一直都得不到糖果
這就會發生線程的飢餓
怎么解決?
我們還有notifyAll方法,與notify功能相同,但是差別在於將會喚醒所有等待線程,這樣所有的等待集合都獲得了一次重生的機會,當然,如果條件不滿足可能繼續進入等待集,如果沒有競爭成功也會在入口集等待
通過notifyAll可以確保沒有人會餓到
notifyAll方法
這也是一個本地方法,看得出來,不管等待還是通知,最終仍舊需要借助於JVM底層。通過操作系統來實現
notifyAll喚醒在此對象監視器上等待的所有線程
與notify除了喚醒線程個數區別外,無任何區別,仍舊是執行結束后,被喚醒的線程才有機會
多線程通信
借助於wait與notify可以完成線程間的通信,可以借助於wait和notifyAll完成多線程之間的通信
其實對於我們最上面的代碼示例中,不僅僅虛假喚醒會出現問題,非虛假喚醒場景下也可能出現問題
在只有一個生產者和消費者時並不會出現問題,但是如果在更多線程場景下,就可能出現問題
比如,兩個生產者A,和B,一個消費者C,執行一段時間后,假設此時隊列已滿
如果A執行時,發現已滿,進入等待
然后B線程執行,仍舊是已滿,進入等待
然后C線程開始執行,消費了一個消息后,調用notify,此時碰巧喚醒了線程A
線程C執行后,線程A競爭成功,進入同步區域執行,線程A生產了一個消息,然后調用notify
不巧的是,此時喚醒的是線程B,線程B醒來以后競爭成功,繼續執行,於是繼續往隊列中添加,也就是調用addLast方法
很顯然,出問題了,出現了已滿但是仍舊調用addLast方法
這種場景下,問題出現在喚醒了一個線程后,其實條件仍舊不滿足,比如上面的描述中,應該喚醒消費者,但是生產者卻被喚醒了,而且此時條件並不滿足
同樣的道理,如果是隊列已經空了,假設有兩個消費者線程A,B,和一個生產者C
消費者A,發現空,wait
消費者B,發現空,wait
生產者C,生產一個消息,notify,喚醒A
A醒來后競爭成功,消費一個消息后,notify,喚醒了B
B醒來后競爭成功,將會繼續消費消息,出現已經空了,但是仍舊會調用removeFirst方法
從結果看,跟虛假喚醒是類似的---醒來時,條件仍舊不滿足
所以解決方法就是將if條件判斷修改為while條件檢測
從這一點也可以看得出來,我們應該總是使用while對條件進行檢測,不僅可以避免虛假喚醒,也能夠避免更多線程並發時的同步問題
如果我們使用了while進行條件檢測
假如說有10個生產者,隊列大小為5,一個消費者
碰巧剛開始是10個生產者運行,接着隊列已滿,10個線程都進入wait狀態
碰巧接下來是消費者不斷消費,持續消費了5個消息,喚醒了其中5個生產者,然后進入wait
如果接下來是這五個生產者喚醒的線程都是剛才進入wait的生產者,會發生什么?
最終所有的生產者都將進入wait狀態!而那個消費者也仍舊是wait!所有的人都在wait,誰來解鎖?
這其中的一個問題就是我們不知道notify將會喚醒哪個線程,有些場景將會導致消費者永遠無法獲得執行的機會
所以應該使用notifyAll,這樣將保障消費者始終有機會執行,哪怕暫時沒機會執行,他仍舊是醒着的,只要她醒着就有機會讓整個車間動起來
如下圖所示,將原來的MessageQueue中的重構為RefactorMessageQueue,其實僅僅修改if為while
測試方法中,隊列設置為5(代碼中使用>判斷,所以實際是6),生產者設置為20個,可以看到很快就死鎖了,並且給線程設置名稱
***************task begin***************
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : queue is full ,waiting...
producer1 : queue is full ,waiting...
producer2 : queue is full ,waiting...
producer3 : queue is full ,waiting...
producer4 : queue is full ,waiting...
producer5 : queue is full ,waiting...
producer6 : queue is full ,waiting...
producer7 : queue is full ,waiting...
producer8 : queue is full ,waiting...
producer9 : queue is full ,waiting...
producer10 : queue is full ,waiting...
producer11 : queue is full ,waiting...
producer12 : queue is full ,waiting...
producer13 : queue is full ,waiting...
producer14 : queue is full ,waiting...
producer15 : queue is full ,waiting...
producer16 : queue is full ,waiting...
producer17 : queue is full ,waiting...
producer18 : queue is full ,waiting...
producer19 : queue is full ,waiting...
consumer : get a message
consumer : get a message
consumer : get a message
consumer : get a message
consumer : get a message
consumer : get a message
consumer : queue is empty ,waiting...
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : queue is full ,waiting...
producer6 : queue is full ,waiting...
producer11 : queue is full ,waiting...
producer10 : queue is full ,waiting...
producer9 : queue is full ,waiting...
producer8 : queue is full ,waiting...
producer7 : queue is full ,waiting...
producer5 : queue is full ,waiting...
producer4 : queue is full ,waiting...
producer3 : queue is full ,waiting...
producer2 : queue is full ,waiting...
producer1 : queue is full ,waiting...
關鍵部分,如下圖,消費者wait后,緊接着生產者滿了,然后就紛紛wait
可以通過Jconsole工具查看
這是官方提供的工具,本地安裝配置過JDK后,可以命令行直接輸入:jconsole即可,然后會打開一個界面窗口
- 命令行輸入jconsole
- 選擇進程,連接
- 點擊線程查看
逐個查看一下每個線程的狀態,你會發現,我們的20個生產者producerX(0-19)以及一個消費者consumer,全部都是:狀態: [B@2368a10b上的WAITING
小結:
多線程場景下,應該總是使用while進行循環條件檢測,並且總是使用notifyAll,而不是notify,以避免出現奇怪的線程問題
總結
wait、notify、notifyAll方法,都需要持有監視器才能夠進行操作,而進入監視器也就是需要在synchronized方法或者代碼塊內,或者借助於顯式鎖同步的代碼塊內
wait的方法簽名中,可以看到將會可能拋出InterruptedException,說明wait是一個可中斷的方法,當其他線程對他進行中斷后(調用interrupt方法)將會拋出異常,並且中斷狀態將會被擦除,被中斷后,該線程相當於被喚醒了
鑒於notify場景下的種種問題,我們應該盡可能的使用notifyAll