[toc]
還記得前面用ArrayList實(shí)現(xiàn)阻塞隊(duì)列的文章:《什么?面試官讓我用ArrayList實(shí)現(xiàn)一個(gè)阻塞隊(duì)列?》。我們通過(guò)synchronized并配合wait和notify實(shí)現(xiàn)了一個(gè)阻塞隊(duì)列。在介紹完前文的synchronized關(guān)鍵字的基本使用之后,本文來(lái)對(duì)這些方法進(jìn)行分析。
1.生產(chǎn)者和消費(fèi)者模型
Producer代碼如下:
public class Producer implements Runnable {
List<Integer> cache;
public Producer(List<Integer> cache) {
new Object();
this.cache = cache;
}
public void put() throws InterruptedException {
synchronized (cache) {
System.out.println(Thread.currentThread().getName()+"獲得鎖");
cache.notify();
while (cache.size() == 1) {
try {
System.out.println(Thread.currentThread().getName()+"wait");
cache.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
TimeUnit.SECONDS.sleep(1);
cache.add(1);
System.out.println(Thread.currentThread().getName() + "生產(chǎn)者寫(xiě)入1條消息");
}
}
@Override
public void run() {
while (true) {
try {
put();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Consumer代碼如下:
public class Consumer implements Runnable {
List<Integer> cache;
public Consumer(List<Integer> cache) {
this.cache = cache;
}
private void consumer() {
synchronized (cache) {
System.out.println(Thread.currentThread().getName()+"獲得鎖");
cache.notify();
while (cache.size() == 0) {
try {
System.out.println(Thread.currentThread().getName()+"wait");
cache.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
cache.remove(0);
System.out.println(Thread.currentThread().getName()+" 消費(fèi)者消費(fèi)了1條消息");
}
}
@Override
public void run() {
while (true) {
consumer();
}
}
}
我們來(lái)調(diào)用上述的生產(chǎn)者和消費(fèi)者模型:
public static void main(String[] args) {
List<Integer> cache = new ArrayList<>();
new Thread(new Producer(cache),"P1").start();
new Thread(new Consumer(cache),"C1").start();
}
啟用了兩個(gè)線程,分別調(diào)用生產(chǎn)者和消費(fèi)者,可以看到這個(gè)過(guò)程交替執(zhí)行:
P1獲得鎖
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1獲得鎖
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
只要生產(chǎn)者產(chǎn)生了數(shù)據(jù),那么消費(fèi)者就能進(jìn)行消費(fèi)。
2. 死鎖產(chǎn)生
還是利用上述代碼,我們來(lái)增加一個(gè)消費(fèi)者:
public static void main(String[] args) {
List<Integer> cache = new ArrayList<>();
new Thread(new Producer(cache),"P1").start();
new Thread(new Consumer(cache),"C1").start();
new Thread(new Consumer(cache),"C2").start();
}
我們發(fā)現(xiàn)程序執(zhí)行了一段時(shí)間之后都停止不動(dòng)了:
P1獲得鎖
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C2獲得鎖
C2 消費(fèi)者消費(fèi)了1條消息
C2獲得鎖
C2wait
C1獲得鎖
C1wait
C2wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
C2wait
在IDEA中,我們對(duì)這三個(gè)線程的狀態(tài)dump了進(jìn)行查看:
"P1" #12 prio=5 os_prio=0 tid=0x0000000020c20800 nid=0x1e9c in Object.wait() [0x00000000219fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ba2eae0> (a java.util.ArrayList)
at java.lang.Object.wait(Object.java:502)
at com.dhb.notify.Producer.put(Producer.java:22)
- locked <0x000000076ba2eae0> (a java.util.ArrayList)
at com.dhb.notify.Producer.run(Producer.java:40)
at java.lang.Thread.run(Thread.java:748)
"C1" #13 prio=5 os_prio=0 tid=0x0000000020c2f000 nid=0x3448 in Object.wait() [0x0000000021aff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ba2eae0> (a java.util.ArrayList)
at java.lang.Object.wait(Object.java:502)
at com.dhb.notify.Consumer.consumer(Consumer.java:21)
- locked <0x000000076ba2eae0> (a java.util.ArrayList)
at com.dhb.notify.Consumer.run(Consumer.java:35)
at java.lang.Thread.run(Thread.java:748)
"C2" #14 prio=5 os_prio=0 tid=0x0000000020c30000 nid=0x904 in Object.wait() [0x0000000021bff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ba2eae0> (a java.util.ArrayList)
at java.lang.Object.wait(Object.java:502)
at com.dhb.notify.Consumer.consumer(Consumer.java:21)
- locked <0x000000076ba2eae0> (a java.util.ArrayList)
at com.dhb.notify.Consumer.run(Consumer.java:35)
at java.lang.Thread.run(Thread.java:748)
可以發(fā)現(xiàn),這三個(gè)線程,都是處于WATTING狀態(tài)。
這說(shuō)明產(chǎn)生了死鎖,沒(méi)有人再去對(duì)線程進(jìn)行喚醒操作。
3.解決問(wèn)題
實(shí)際上,上述問(wèn)題的解決方式很簡(jiǎn)單,只需要在Consumer和Producer中將notify方法都緩沖notifyAll方法就能解決。我們?cè)诤竺嬲鹿?jié)來(lái)分析產(chǎn)生死鎖的具體原因,在此先看看修改代碼后的執(zhí)行效果.
將所有
cache.notify();
替換為:
cache.notifyAll();
執(zhí)行效果:
P1獲得鎖
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1獲得鎖
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
C2獲得鎖
C2wait
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
C2wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C2 消費(fèi)者消費(fèi)了1條消息
C2獲得鎖
C2wait
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
C2wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C2 消費(fèi)者消費(fèi)了1條消息
C2獲得鎖
C2wait
C1wait
可以看到上述程序可以正常運(yùn)行,沒(méi)有任何死鎖問(wèn)題。
4.notify和notifyAll區(qū)別
這才是本文的關(guān)鍵知識(shí)點(diǎn),通過(guò)前面這兩個(gè)示例,相信大家都能看出來(lái),notify會(huì)造成死鎖,而notify則不會(huì)。這是因?yàn)?,在前面我們分析過(guò)Object的源碼,在注釋中,就提到,notify只會(huì)選擇等待隊(duì)列其中之一的線程,將其變?yōu)樽枞麪顟B(tài),等待獲得CPU的執(zhí)行權(quán)。而NotifyAll則是將等待隊(duì)列全部的線程都添加到等EntryList,然后這些線程都會(huì)等待獲得CPU的執(zhí)行時(shí)間。
實(shí)際上,我們?cè)谇拔姆治鯰hread源碼的時(shí)候,對(duì)線程的各自運(yùn)行狀態(tài)進(jìn)行了總結(jié):

在java中線程的運(yùn)行狀態(tài)共有6種。而wait則是將線程從RUNNING變?yōu)閃AITING狀態(tài)。而notify和notifyAll則是將線程從WAITING狀態(tài)變?yōu)镽UNNING的方法。實(shí)際上,首先這個(gè)轉(zhuǎn)換過(guò)程需要再經(jīng)過(guò)BLOCK狀態(tài)轉(zhuǎn)換。因?yàn)橹挥胁糠志€程能獲得鎖,進(jìn)入執(zhí)行狀態(tài),而獲得不到的,自然進(jìn)入了BLOCK狀態(tài)。
notify只會(huì)選擇等待隊(duì)列的一個(gè)線程,這個(gè)過(guò)程是不確定的,由的jvm可能是隨機(jī)選擇,而hotspot再1.8種,直接是從WaitSet的頭部拿到第一個(gè)線程。
notify過(guò)程如下圖所示:

而notifyAll則不同于notify,由于會(huì)將全部wait的線程都進(jìn)入EntryList隊(duì)列,這個(gè)過(guò)程如下:

這就是這兩個(gè)方法的區(qū)別。
5.死鎖產(chǎn)生的原因
5.1 兩個(gè)線程
首先看看兩個(gè)線程的時(shí)候:
P1獲得鎖
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1獲得鎖
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
這個(gè)過(guò)程日志可以用下圖表示:
-
由于兩個(gè)線程都啟動(dòng)了,P1首先獲得鎖,那么C1阻塞。狀態(tài)為P1執(zhí)行,C1阻塞。
P1執(zhí)行 C1阻塞
-
-
2.再這之后,P1執(zhí)行完調(diào)用wait方法,C1獲得鎖,那么此時(shí)C1執(zhí)行,P1等待:
C1執(zhí)行 P1等待
-
3.之后,C1調(diào)用notify方法將P1喚醒,由于C1還需要執(zhí)行后續(xù)相關(guān)代碼,那么此時(shí)P1進(jìn)入阻塞隊(duì)列
C1 notify P1 -
4.隨后,C1執(zhí)行完畢進(jìn)入wait狀態(tài),這時(shí)候P1重寫(xiě)獲得鎖:
P1執(zhí)行 C1等待 -
等P1執(zhí)行的時(shí)候,又會(huì)再次notify C1
P1 notify C1
上述過(guò)程就會(huì)不斷循環(huán),這樣線程就不會(huì)卡頓。會(huì)一直執(zhí)行下去。
5.2 三個(gè)線程
那么現(xiàn)在換成三個(gè)線程,我們來(lái)看看日志:
P1獲得鎖
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C2獲得鎖
C2 消費(fèi)者消費(fèi)了1條消息
C2獲得鎖
C2wait
C1獲得鎖
C1wait
C2wait
P1生產(chǎn)者寫(xiě)入1條消息
P1獲得鎖
P1wait
C1 消費(fèi)者消費(fèi)了1條消息
C1獲得鎖
C1wait
C2wait
上述過(guò)程我們用畫(huà)圖的方式來(lái)進(jìn)行:
-
1.P1首先獲得鎖,而C1、C2阻塞。P1雖然會(huì)調(diào)用notify但是waitSet沒(méi)有線程。
P1執(zhí)行 C1C2阻塞 -
2.之后C2獲得了鎖,此時(shí)P1等待,C1阻塞
C2執(zhí)行 P1等待 C1阻塞 -
3.此后C2調(diào)用Notify方法,將P1變?yōu)樽枞麪顟B(tài)。然后消費(fèi)數(shù)據(jù)。此時(shí)cache長(zhǎng)度為0。
C2 notify P1 -
4.此后C1獲得鎖,此時(shí)C2等待,P1阻塞。
C1 執(zhí)行,C2等待 P1阻塞 -
5.此時(shí)C1調(diào)用notify將C2變?yōu)樽枞?。此時(shí)while循環(huán)由于cache長(zhǎng)度為0,因此C1會(huì)調(diào)用wait方法。
C1 notify C2 -
6.此后,P1再次獲得鎖。此時(shí)C1等待,C2執(zhí)行。
P1 執(zhí)行,C1等待、C2阻塞 -
7.從此時(shí)開(kāi)始,每個(gè)線程的執(zhí)行都需要注意了,此時(shí)線程再次獲得鎖的時(shí)候只會(huì)執(zhí)行wait之后的代碼。對(duì)于P1,由于cache的size為0,因此會(huì)繼續(xù)寫(xiě)入數(shù)據(jù)。之后再次進(jìn)入循環(huán)會(huì)發(fā)notify。將C1變?yōu)樽枞?/p>
P1 notify C1 -
8.之后 C1獲得鎖,而P1等待,C2阻塞
C1 執(zhí)行,P1等待、C2阻塞 9.C1再次獲得鎖,只會(huì)執(zhí)行wait之后的內(nèi)容:
while (cache.size() == 0) {
try {
System.out.println(Thread.currentThread().getName()+"wait");
cache.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
可以看到這個(gè)代碼,當(dāng)從wait方法之后由于cache.size為0,再次進(jìn)入wait狀態(tài),這里沒(méi)辦法調(diào)用notify了。

-
10.之后C2也再次獲得鎖,但是與C1一樣,由于條件不滿足,不會(huì)執(zhí)行notify。
C2while條件不滿足繼續(xù)wait
這樣就對(duì)線程死鎖進(jìn)行了復(fù)盤(pán)??梢钥吹剑琻otify導(dǎo)致問(wèn)題的原因是,如果我們的條件沒(méi)設(shè)置好,這會(huì)導(dǎo)致線程沒(méi)辦法去執(zhí)行notify操作。而這樣的話所有的線程都可能進(jìn)入wait狀態(tài)。再也沒(méi)有人來(lái)調(diào)用notify,這就導(dǎo)致了死鎖。悲劇就這樣發(fā)生了。這也進(jìn)一步說(shuō)明,notify方法是非常脆弱的,如果我們的代碼種在同一個(gè)鎖上的競(jìng)爭(zhēng)的線程只有2個(gè)的話,notify是完全能勝任的。但是如果超過(guò)2個(gè),就會(huì)因?yàn)闂l件設(shè)置不合理而導(dǎo)致了死鎖。
而notify,則是無(wú)論什么時(shí)候,只要被調(diào)用,就會(huì)將所有的線程全部移動(dòng)到阻塞隊(duì)列等待鎖。只要有一個(gè)線程調(diào)用notifyAll就能將所有的線程喚醒。
5.總結(jié)
本文對(duì)notify和notifyAll方法進(jìn)行了分析,需要注意的是,notify和notifyAll方法的區(qū)別,一個(gè)是喚醒其中之一的等待線程,不同的JVM實(shí)現(xiàn)的方式不同,而HotSpot源碼中,是從WaitSet中取的head元素。也就是說(shuō),誰(shuí)先進(jìn)入Wait狀態(tài)則就會(huì)將誰(shuí)notify出來(lái)。而notifyAll則是將全部的線程都從WaitSet取出。這樣就不會(huì)有線程等待。通過(guò)上述分析可見(jiàn),產(chǎn)生死鎖的根本原因還是在條件變量的控制。
但是需要注意的是,雖然從WaitSet拿到元素不一定隨機(jī)。但是,多個(gè)線程對(duì)鎖的競(jìng)爭(zhēng)的情況確是不一定的。上述的生產(chǎn)者消費(fèi)者模型也只能用于模擬演示,因?yàn)橛锌赡?,生產(chǎn)者線程可能一直搶不到鎖,導(dǎo)致全部都是消費(fèi)者線程互相爭(zhēng)搶。













