在鎖與監視器中有對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