多線程協作wait、notify、notifyAll方法簡介理解使用 多線程中篇(十四)


在鎖與監視器中有對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 {
}
image_5c7c7886_7f2
ps:判斷條件 if (messageQueue.size() > max) 所以實際隊列空間為4
 
從以上代碼示例中可以看得出來,借助於鎖lock,實現了生產者和消費者之間的通信與互斥
他們都是基於這個臨界資源進行管理的,這個鎖就相當於調度的中心,進入了監視器之后如果條件滿足,那么執行,並且會通知其他線程,如果不滿足則會等待。
從這個例子中應該可以理解,鎖與監視器 和 線程通信之間的關系

wait方法

image_5c7c7886_2cd0
有三個版本的wait方法,wait,表示在等待此鎖(等待持有這個鎖對象對應的監視器)
對於無參數的wait以及雙參數的wait,可以查看源代碼,核心為這個native方法
wait()直接調用wait(0);
wait(long timeout, int nanos)在參數有效性校驗后調用wait(timeout)
image_5c7c7886_c15
深入看下native方法
image_5c7c7886_291b
API解釋:
在其他線程調用此對象的 notify() 方法或 notifyAll() 方法或者超過指定的時間量前導致當前線程等待。 
如前面所述,wait以及notify以及notifyAll都需要持有監視器才可以調用該方法
既然另外兩個版本都是依賴底層的這個wait,所以所有版本的wait都需要持有監視器
一旦該方法調用,將會進入該監視器的等待集,並且放棄同步要求(也就是不再持有鎖,將會釋放鎖)
一定注意:將會釋放鎖,將會釋放鎖,會釋放鎖......
image_5c7c7886_743a
除非遇到上面的這幾種情況,否則將會線程被禁用,進入休眠狀態,也就是持續等待
遇到這幾種情況后,將會從對象的等待集中刪除線程,並重新進行線程調度
需要注意的是從等待集中刪除並不意味着立馬執行,他仍舊需要與其他線程競爭,如果競爭失敗,也會繼續等待
如果一個線程在不止一個鎖對象的等待集內,那么將只是解除當前這個鎖對象等待集中解鎖,在其他等待集中仍舊是鎖定的,如果你在多個等待集合中,總不能一下子就從所有的等待集合中釋放,對吧
如果在等待時,任何其他的線程中斷了該線程,那么將會收到一個異常,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方法

image_5c7c7886_7439
notify也是一個本地方法,他將會喚醒在該監視器上等待的某個線程(關鍵詞:當前監視器、某一個線程)
即使在該監視器上有多個線程正在等待,那么也是僅僅喚醒一個
而且,選擇是任意的
另外還需要注意,是這邊notify之后,那么立刻就有什么反應了嗎?不是的!
只有當前持有監視器的線程執行結束,才有機會執行被喚醒的線程,而且被喚醒的線程仍舊需要參與競爭(如果入口集中還有線程在等待的話)
所以,如果一個1000行的方法,不管你在哪一行執行notify,終歸是要方法結束后,被喚醒的線程才有機會
notify問題
notify僅僅喚醒其中一個線程,而且,這種機制是非公平的,也就是說不能夠保障每個線程必然都有機會獲得執行。
換個說法,比如10個小朋友等待老師發糖果,如果每次都隨機選一個,可能有的小朋友一直都得不到糖果
這就會發生線程的飢餓
怎么解決?
我們還有notifyAll方法,與notify功能相同,但是差別在於將會喚醒所有等待線程,這樣所有的等待集合都獲得了一次重生的機會,當然,如果條件不滿足可能繼續進入等待集,如果沒有競爭成功也會在入口集等待
通過notifyAll可以確保沒有人會餓到

notifyAll方法

image_5c7c7886_3858
這也是一個本地方法,看得出來,不管等待還是通知,最終仍舊需要借助於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個,可以看到很快就死鎖了,並且給線程設置名稱
image_5c7c7886_2719
 
***************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
image_5c7c7887_5328
可以通過Jconsole工具查看
這是官方提供的工具,本地安裝配置過JDK后,可以命令行直接輸入:jconsole即可,然后會打開一個界面窗口
  1. 命令行輸入jconsole
  2. 選擇進程,連接
  3. 點擊線程查看
image_5c7c7887_28f0
逐個查看一下每個線程的狀態,你會發現,我們的20個生產者producerX(0-19)以及一個消費者consumer,全部都是:狀態: [B@2368a10b上的WAITING
image_5c7c7887_10ee
小結:
多線程場景下,應該總是使用while進行循環條件檢測,並且總是使用notifyAll,而不是notify,以避免出現奇怪的線程問題

總結

wait、notify、notifyAll方法,都需要持有監視器才能夠進行操作,而進入監視器也就是需要在synchronized方法或者代碼塊內,或者借助於顯式鎖同步的代碼塊內
wait的方法簽名中,可以看到將會可能拋出InterruptedException,說明wait是一個可中斷的方法,當其他線程對他進行中斷后(調用interrupt方法)將會拋出異常,並且中斷狀態將會被擦除,被中斷后,該線程相當於被喚醒了  
鑒於notify場景下的種種問題,我們應該盡可能的使用notifyAll
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM