前言
關(guān)于wait/notify/notifyall有2個(gè)經(jīng)典的面試:
- notify和notifyall有什么區(qū)別在抛?
- 為什么wait方法要寫(xiě)在while循環(huán)里面而不是if呢?
帶著這2個(gè)問(wèn)題娇唯,我們來(lái)學(xué)習(xí)下synchronized提供的等待通知機(jī)制。
1拂铡、你要知道的基本知識(shí)
- wait/notify/notifyall都是存在與Object類里面的方法熏瞄。
ps:為什么要存在Object里面而不是Thread里面呢?趕緊看下這篇吧(<u>大徹大悟synchronized原理也搓,鎖的升級(jí)</u>)赏廓,從底層了解其原因。
- wait方法:wait方法只是無(wú)參和帶超時(shí)時(shí)間2種方法傍妒,調(diào)用wait方法的的線程會(huì)進(jìn)入waiting或timed_waiting狀態(tài)幔摸;
- notify方法:調(diào)用notify方法的線程,會(huì)喚醒一個(gè)處于waiting狀態(tài)的線程颤练;
- notifyall方法:調(diào)用notify方法的線程既忆,會(huì)喚醒所有處于waiting狀態(tài)的線程;
有一個(gè)前提嗦玖,就是wait/notify/notifyall方法必須在獲取到synchronized資源鎖的情況下患雇,才能調(diào)用,也就是wait/notify/notifyall必須在synchronized代碼塊里面宇挫。
使用wait/notify/notifyal有個(gè)經(jīng)典的范式苛吱,這便是上面的第二個(gè)問(wèn)題。
while(條件不滿足) {
wait();
}
2器瘪、典型的生產(chǎn)者消費(fèi)者模式的樣例
現(xiàn)在我們寫(xiě)個(gè)典型的生產(chǎn)者消費(fèi)者模式的樣例:
- 有2個(gè)生產(chǎn)者翠储,當(dāng)list不滿的情況下绘雁,往list里面添加數(shù)據(jù),當(dāng)list滿的情況下調(diào)用wait方法援所。每添加一條數(shù)據(jù)庐舟,就調(diào)用notifyAll()方法。
- 2個(gè)消費(fèi)者住拭,當(dāng)list不為空的情況下挪略,消費(fèi)list里面的第一個(gè)元素,并且調(diào)用notifyAll()方法滔岳。
- 為了效果杠娱,我讓每個(gè)生產(chǎn)者只生產(chǎn)了3條數(shù)據(jù),消費(fèi)者一直在消費(fèi)澈蟆,最終都進(jìn)入waiting狀態(tài)墨辛。
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class WaitNotifyTest {
private List<String> list = new ArrayList<>();
public static void main(String[] args) {
WaitNotifyTest waitNotifyTest = new WaitNotifyTest();
Thread producer1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
waitNotifyTest.produce();
}
});
producer1.setName("生產(chǎn)者1號(hào)");
Thread producer2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
waitNotifyTest.produce();
}
});
producer2.setName("生產(chǎn)者2號(hào)");
Thread consumer1 = new Thread(() -> {
while (true) waitNotifyTest.consume();
});
consumer1.setName("消費(fèi)者1號(hào)");
Thread consumer2 = new Thread(() -> {
while (true) waitNotifyTest.consume();
});
consumer2.setName("消費(fèi)者2號(hào)");
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
}
private void produce() {
synchronized (this) {
while (listIsFull()) {
try {
System.out.println(Thread.currentThread().getName() + "進(jìn)入等待池");
wait();
System.out.println(Thread.currentThread().getName() + "被喚醒了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String value = UUID.randomUUID().toString();
System.out.println(Thread.currentThread().getName() + "生產(chǎn)了一條消息:" + value);
list.add(value);
notifyAll();
}
}
private void consume() {
synchronized (this) {
while (listIsEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + "進(jìn)入等待池");
wait();
System.out.println(Thread.currentThread().getName() + "被喚醒了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消費(fèi)了一條消息:" + list.get(0));
list.remove(0);
notifyAll();
}
}
private boolean listIsFull() {
return list.size() == 1;
}
private boolean listIsEmpty() {
return list.size() == 0;
}
}
執(zhí)行結(jié)果:
生產(chǎn)者1號(hào)生產(chǎn)了一條消息:85158920-3784-4c5f-9c2a-2961aea75086
消費(fèi)者2號(hào)消費(fèi)了一條消息:85158920-3784-4c5f-9c2a-2961aea75086
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者2號(hào)生產(chǎn)了一條消息:3caf8352-28d3-44f6-8a7f-5ecf7356e903
生產(chǎn)者2號(hào)進(jìn)入等待池
消費(fèi)者1號(hào)消費(fèi)了一條消息:3caf8352-28d3-44f6-8a7f-5ecf7356e903
消費(fèi)者1號(hào)進(jìn)入等待池
生產(chǎn)者2號(hào)被喚醒了
生產(chǎn)者2號(hào)生產(chǎn)了一條消息:b8764c83-8b42-4527-a953-4424e1103111
生產(chǎn)者2號(hào)進(jìn)入等待池
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)消費(fèi)了一條消息:b8764c83-8b42-4527-a953-4424e1103111
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者1號(hào)生產(chǎn)了一條消息:32228e02-d7f1-4866-83c7-d3fcfb6a51b4
生產(chǎn)者1號(hào)進(jìn)入等待池
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)消費(fèi)了一條消息:32228e02-d7f1-4866-83c7-d3fcfb6a51b4
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者2號(hào)被喚醒了
生產(chǎn)者2號(hào)生產(chǎn)了一條消息:3af2d730-7bf3-4a26-8a96-f6f3f30fc39a
消費(fèi)者1號(hào)被喚醒了
消費(fèi)者1號(hào)消費(fèi)了一條消息:3af2d730-7bf3-4a26-8a96-f6f3f30fc39a
消費(fèi)者1號(hào)進(jìn)入等待池
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)進(jìn)入等待池
生產(chǎn)者1號(hào)被喚醒了
生產(chǎn)者1號(hào)生產(chǎn)了一條消息:0867b9ae-de1a-486b-b0b0-e56c6b10a49b
消費(fèi)者2號(hào)被喚醒了
消費(fèi)者2號(hào)消費(fèi)了一條消息:0867b9ae-de1a-486b-b0b0-e56c6b10a49b
消費(fèi)者2號(hào)進(jìn)入等待池
消費(fèi)者1號(hào)被喚醒了
消費(fèi)者1號(hào)進(jìn)入等待池
3、notify和notifyAll有什么區(qū)別
最簡(jiǎn)單的回答就是:notify只喚醒一個(gè)waiting狀態(tài)的線程趴俘,notifyAll喚醒所有waiting狀態(tài)的線程睹簇。
但是要徹底弄清楚它們的區(qū)別,還是要從synchronized的底層說(shuō)起寥闪√荩看過(guò)這篇文章的顯然已經(jīng)知道答案了。
[圖片上傳失敗...(image-2d12eb-1605322860624)]
我這里再整理下疲憋。
- synchronized維護(hù)的對(duì)象鎖有2個(gè)隊(duì)列凿渊,一個(gè)_EntryList,一個(gè)_WaitSet缚柳。
- 加鎖時(shí)埃脏,線程獲取到鎖進(jìn)入臨界區(qū)(_owner),若線程獲取不到鎖秋忙,便加入_EntryList彩掐,進(jìn)入blocked阻塞狀態(tài)。
- 線程獲取到鎖后灰追,調(diào)用wait方法堵幽,被加入_WaitSet隊(duì)列,進(jìn)入waiting狀態(tài)弹澎,然后等待喚醒朴下。當(dāng)線程被喚醒的時(shí)候,被喚醒的線程需要再次獲取對(duì)象鎖苦蒿。
- 喚醒線程殴胧,我們可以調(diào)用notify和notifyAll方法,notify只是隨機(jī)的喚醒一個(gè)_WaitSet中的線程佩迟。notifyAll會(huì)喚醒所有處于_WaitSet中的線程溃肪。
- 不管喚醒一個(gè)線程免胃,還是喚醒多個(gè)線程,最終獲得對(duì)象鎖的惫撰,只有一個(gè)線程。如果_EntryList同時(shí)存在競(jìng)爭(zhēng)鎖資源的線程躺涝,那么被喚醒的線程還需要和_EntryList中的線程一起競(jìng)爭(zhēng)鎖資源厨钻。但是JVM保證最終只會(huì)讓一個(gè)線程獲取到鎖。
那如果只喚醒一個(gè)線程會(huì)有什么問(wèn)題呢坚嗜?
拿上面的生產(chǎn)者消費(fèi)者舉個(gè)例子:
- 當(dāng)list為空時(shí)夯膀,消費(fèi)者consumer1、consumer2都會(huì)處于waiting狀態(tài)
- 生產(chǎn)者producer1和生產(chǎn)者producer2競(jìng)爭(zhēng)鎖苍蔬,生產(chǎn)者producer1先拿到鎖诱建,生產(chǎn)一條數(shù)據(jù),調(diào)用notify(假設(shè)喚醒的是消費(fèi)者consumer1)碟绑,然后producer1進(jìn)入阻塞blocked狀態(tài)俺猿,并釋放鎖;
- 消費(fèi)者consumer1被喚醒后格仲,有三個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖(producer1押袍、producer2、consumer1)凯肋,假設(shè)producer2獲得鎖谊惭,producer2發(fā)現(xiàn)list滿了,然后進(jìn)入waiting狀態(tài)侮东,并釋放鎖圈盔;
- 鎖被釋放后,有兩個(gè)個(gè)線程同時(shí)競(jìng)爭(zhēng)鎖(producer1悄雅、consumer1)驱敲,假設(shè)consumer1獲取到鎖,consumer1消費(fèi)消息煤伟,然后調(diào)用notify(此時(shí)consumer2和生產(chǎn)者producer2處于waiting)癌佩;
問(wèn)題就出在這里:
假設(shè)喚醒的是生產(chǎn)者producer2,沒(méi)有問(wèn)題便锨;
假設(shè)喚醒的是消費(fèi)者consumer2围辙,那consumer2會(huì)發(fā)現(xiàn)list任然為空,繼續(xù)進(jìn)入waiting狀態(tài)放案。但是呢姚建,恰好之前進(jìn)入阻塞狀態(tài)的producer1已經(jīng)下線(在這個(gè)例子中就是生產(chǎn)了3條數(shù)據(jù))。這樣就出現(xiàn)了死鎖了(producer2和consumer2都處于waiting狀態(tài)吱殉,消費(fèi)者consumer1在獲取到鎖后也會(huì)進(jìn)入wait狀態(tài))
所以說(shuō)掸冤,使用notify某些希望被喚醒的線程厘托,永遠(yuǎn)得不到喚醒,獲取不到鎖資源稿湿,導(dǎo)致死鎖铅匹。
綜上所述:我們盡量使用notifyAll而不是notify。除非你經(jīng)過(guò)深思熟慮饺藤,且明確知道喚醒的就你希望的線程(比如上例中只有一個(gè)生產(chǎn)者一個(gè)消費(fèi)者)
4包斑、為什么wait方法要寫(xiě)在while循環(huán)里面而不是if呢
再來(lái)看下第二個(gè)問(wèn)題:為什么wait方法要寫(xiě)在while循環(huán)里面而不是if呢?
private void produce() {
synchronized (this) {
while (listIsFull()) {
try {
System.out.println(Thread.currentThread().getName() + "進(jìn)入等待池");
wait();
System.out.println(Thread.currentThread().getName() + "被喚醒了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String value = UUID.randomUUID().toString();
System.out.println(Thread.currentThread().getName() + "生產(chǎn)了一條消息:" + value);
list.add(value);
notifyAll();
}
}
明確這2點(diǎn):
- 線程被喚醒之后涕俗,代碼是緊接著執(zhí)行wait后面的代碼(從上面的執(zhí)行結(jié)果可以看出)罗丰;
- 進(jìn)入waiting狀態(tài)的線程被喚醒的條件是“條件滿足”,對(duì)應(yīng)到下面的例子就是隊(duì)列不滿再姑。
被喚醒的線程需要和其他線程競(jìng)爭(zhēng)鎖資源(最終只有一個(gè)線程獲取到)萌抵,那么當(dāng)被喚醒的線程獲得鎖資源的時(shí)候,之前的條件可能又不滿足了元镀。
如果while改成if绍填,那么被喚醒的線程繼續(xù)執(zhí)行(默認(rèn)條件任然滿足),這明顯會(huì)導(dǎo)致并發(fā)問(wèn)題凹联,比如超額生產(chǎn)沐兰、消費(fèi)。
5蔽挠、總結(jié)
以后遇到同樣的問(wèn)題知道怎么回答了吧住闯!
ps:一天一個(gè)IDEA小技巧
快捷鍵[Alt+7]可以打開(kāi)當(dāng)前類的架構(gòu)圖(Structure),可以快速查看類澳淑、方法比原、字段等。這樣可以提升工作效率哦杠巡,不要用鼠標(biāo)滾動(dòng)查找啦A烤健!氢拥!
例:
多線程連載:
<u>Java內(nèi)存模型-volatile的應(yīng)用(實(shí)例講解)</u>
<u>synchronized的三種應(yīng)用方式(實(shí)例講解)</u>
<u>可重入鎖-synchronized是可重入鎖嗎蚌铜?</u>
<u>大徹大悟synchronized原理,鎖的升級(jí)</u>
<u>一文弄懂Java的線程池</u>
<u>公平鎖和非公平鎖-ReentrantLock是如何實(shí)現(xiàn)公平嫩海、非公平的</u>
<u>一圖全面了解Java線程的生命周期</u>
<u>守護(hù)線程和用戶線程的真正區(qū)別(實(shí)例講解)</u>