Java并發(fā)編程:使用Wait和Notify方法的注意事項(xiàng)
在之前的講解線程狀態(tài)的文章中,我們提到了wait和notify方法可以讓線程在運(yùn)行狀態(tài)和等待狀態(tài)之間轉(zhuǎn)換。在這篇文章中,我們將深入探討wait、notify和notifyAll方法在使用中的注意事項(xiàng)。我們主要從三個(gè)問(wèn)題入手:
- 為什么wait方法必須在synchronized保護(hù)的代碼中使用?
- 為什么wait方法需要在循環(huán)操作中使用?
- wait/notify和sleep方法有什么異同?
1. 為什么wait()方法必須在synchronized修飾的代碼中使用?
為了找到這個(gè)問(wèn)題的答案,我們不妨反過(guò)來(lái)思考:如果不要求在synchronized代碼中使用wait方法,會(huì)出現(xiàn)什么問(wèn)題呢?讓我們來(lái)看這段代碼。
public class QueueDemo {
Queue<String> buffer = new LinkedList<String>();
public void save(String data) {
buffer.add(data);
notify(); // 因?yàn)榭赡苡芯€程在 take() 方法中等待
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
在這段代碼中,有兩個(gè)方法。save方法負(fù)責(zé)向緩沖區(qū)添加數(shù)據(jù),然后執(zhí)行notify方法來(lái)喚醒之前等待的線程。take方法負(fù)責(zé)檢查緩沖區(qū)是否為空。如果為空,線程進(jìn)入等待狀態(tài);如果不為空,線程從緩沖區(qū)中取出數(shù)據(jù)。
這段代碼沒(méi)有使用synchronized保護(hù),可能會(huì)出現(xiàn)以下情況:
- 首先,消費(fèi)者線程調(diào)用take方法,并判斷buffer.isEmpty是否返回true。如果返回true,表示緩沖區(qū)為空,線程準(zhǔn)備進(jìn)入等待狀態(tài)。然而,在線程調(diào)用wait方法之前,它被可能已經(jīng)被掛起了,wait方法沒(méi)有執(zhí)行。
- 此時(shí),生產(chǎn)者線程開(kāi)始運(yùn)行,并執(zhí)行了整個(gè)save方法。它向緩沖區(qū)添加了數(shù)據(jù),并執(zhí)行了notify方法,但notify沒(méi)有效果,因?yàn)橄M(fèi)者線程的wait方法還沒(méi)有執(zhí)行,所以沒(méi)有線程在等待被喚醒。
- 隨后,之前被掛起的消費(fèi)者線程恢復(fù)執(zhí)行,并調(diào)用了wait方法,進(jìn)入等待狀態(tài)。
出現(xiàn)這個(gè)問(wèn)題的原因是這里的“判斷 - 執(zhí)行”不是原子操作,它在中間被中斷,是線程不安全的。
假設(shè)此時(shí)沒(méi)有更多的生產(chǎn)者進(jìn)行生產(chǎn),消費(fèi)者可能會(huì)陷入無(wú)限等待,因?yàn)樗e(cuò)過(guò)了save方法中的notify喚醒。
你可以模擬一個(gè)生產(chǎn)者線程和一個(gè)消費(fèi)者線程分別調(diào)用這兩個(gè)方法:
public class QueueDemo2 {
Queue<String> buffer = new LinkedList<>();
public void save(String data) {
System.out.println("Produce a data");
buffer.add(data);
notify(); // 因?yàn)榭赡苡腥嗽?take() 中等待
}
public String take() throws InterruptedException {
System.out.println("Try to consume a data");
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
public static void main(String[] args) throws InterruptedException {
QueueDemo2 queueDemo = new QueueDemo2();
Thread producerThread = new Thread(() -> {
queueDemo.save("Hello World!");
});
Thread consumerThread = new Thread(() -> {
try {
System.out.println(queueDemo.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumerThread.start();
producerThread.start();
}
}
你可以嘗試執(zhí)行這段代碼,看看是否會(huì)出現(xiàn)之前提到的問(wèn)題。
實(shí)際輸出如下:
Try to consume a data
Produce a data
Exception in thread "Thread-0" Exception in thread "Thread-1"
java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at thread.basic.chapter4.QueueDemo2.save(QueueDemo2.java:13)
at thread.basic.chapter4.QueueDemo2.lambda$main$0(QueueDemo2.java:28)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at thread.basic.chapter4.QueueDemo2.take(QueueDemo2.java:19)
at thread.basic.chapter4.QueueDemo2.lambda$main$1(QueueDemo2.java:33)
根本沒(méi)有犯錯(cuò)的機(jī)會(huì)。wait方法和notify方法在沒(méi)有synchronized保護(hù)的代碼塊中執(zhí)行時(shí),會(huì)直接拋出java.lang.IllegalMonitorStateException異常。
修改代碼:
public class SyncQueueDemo2 {
Queue<String> buffer = new LinkedList<>();
public synchronized void save(String data) {
System.out.println("Produce a data");
buffer.add(data);
notify(); // 因?yàn)榭赡苡腥嗽?take() 中等待
}
public synchronized String take() throws InterruptedException {
System.out.println("Try to consume a data");
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
public static void main(String[] args) throws InterruptedException {
SyncQueueDemo2 queueDemo = new SyncQueueDemo2();
Thread producerThread = new Thread(() -> {
queueDemo.save("Hello World!");
});
Thread consumerThread = new Thread(() -> {
try {
System.out.println(queueDemo.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumerThread.start();
producerThread.start();
}
}
再次執(zhí)行代碼,輸出如下:
Produce a data
Try to consume a data
Hello World!
可以看到,生產(chǎn)的"Hello World!"已經(jīng)被成功消費(fèi)并打印到控制臺(tái)。
2. 為什么wait方法需要在循環(huán)操作中使用?
線程調(diào)用wait方法后,可能會(huì)出現(xiàn)虛假喚醒(spurious wakeup)的情況,即線程在沒(méi)有被notify/notifyAll調(diào)用、沒(méi)有被中斷、也沒(méi)有超時(shí)的情況下被喚醒,這是我們不希望發(fā)生的情況。
雖然在真實(shí)環(huán)境中,虛假喚醒的概率非常小,但程序仍然需要在虛假喚醒的情況下保證正確性,因此需要使用while循環(huán)結(jié)構(gòu)。
while (條件不滿足) {
obj.wait();
}
這樣,即使線程被虛假喚醒,如果條件不滿足,wait會(huì)繼續(xù)執(zhí)行,從而消除虛假喚醒導(dǎo)致的風(fēng)險(xiǎn)。
3.wait/notify和sleep方法有什么異同?
wait方法和sleep方法的相同點(diǎn)如下:
- 它們都可以阻塞線程。
- 它們都可以響應(yīng)中斷:如果在等待過(guò)程中收到中斷信號(hào),它們會(huì)響應(yīng)并拋出InterruptedException異常。
它們之間也有很多不同點(diǎn):
- wait方法必須在synchronized保護(hù)的代碼中使用,而sleep方法沒(méi)有這個(gè)要求。
- 當(dāng)sleep方法在synchronized代碼中執(zhí)行時(shí),它不會(huì)釋放鎖,而wait方法會(huì)主動(dòng)釋放鎖。
- sleep方法需要定義一個(gè)時(shí)間,時(shí)間到期后線程會(huì)主動(dòng)恢復(fù)。對(duì)于沒(méi)有參數(shù)的wait方法,它意味著永久等待,直到被中斷或喚醒,不會(huì)主動(dòng)恢復(fù)。
- wait和notify是Object類的方法,而sleep是Thread類的方法。
好了,這次的內(nèi)容就到這里,下次再見(jiàn)!