Java并發編程:使用Wait和Notify方法的注意事項
在之前的講解線程狀態的文章中,我們提到了wait和notify方法可以讓線程在運行狀態和等待狀態之間轉換。在這篇文章中,我們將深入探討wait、notify和notifyAll方法在使用中的注意事項。我們主要從三個問題入手:
- 為什么wait方法必須在synchronized保護的代碼中使用?
- 為什么wait方法需要在循環操作中使用?
- wait/notify和sleep方法有什么異同?
1. 為什么wait()方法必須在synchronized修飾的代碼中使用?
為了找到這個問題的答案,我們不妨反過來思考:如果不要求在synchronized代碼中使用wait方法,會出現什么問題呢?讓我們來看這段代碼。
public class QueueDemo {
Queue<String> buffer = new LinkedList<String>();
public void save(String data) {
buffer.add(data);
notify(); // 因為可能有線程在 take() 方法中等待
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
在這段代碼中,有兩個方法。save方法負責向緩沖區添加數據,然后執行notify方法來喚醒之前等待的線程。take方法負責檢查緩沖區是否為空。如果為空,線程進入等待狀態;如果不為空,線程從緩沖區中取出數據。
這段代碼沒有使用synchronized保護,可能會出現以下情況:
- 首先,消費者線程調用take方法,并判斷buffer.isEmpty是否返回true。如果返回true,表示緩沖區為空,線程準備進入等待狀態。然而,在線程調用wait方法之前,它被可能已經被掛起了,wait方法沒有執行。
- 此時,生產者線程開始運行,并執行了整個save方法。它向緩沖區添加了數據,并執行了notify方法,但notify沒有效果,因為消費者線程的wait方法還沒有執行,所以沒有線程在等待被喚醒。
- 隨后,之前被掛起的消費者線程恢復執行,并調用了wait方法,進入等待狀態。
出現這個問題的原因是這里的“判斷 - 執行”不是原子操作,它在中間被中斷,是線程不安全的。
假設此時沒有更多的生產者進行生產,消費者可能會陷入無限等待,因為它錯過了save方法中的notify喚醒。
你可以模擬一個生產者線程和一個消費者線程分別調用這兩個方法:
public class QueueDemo2 {
Queue<String> buffer = new LinkedList<>();
public void save(String data) {
System.out.println("Produce a data");
buffer.add(data);
notify(); // 因為可能有人在 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();
}
}
你可以嘗試執行這段代碼,看看是否會出現之前提到的問題。
實際輸出如下:
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)
根本沒有犯錯的機會。wait方法和notify方法在沒有synchronized保護的代碼塊中執行時,會直接拋出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(); // 因為可能有人在 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();
}
}
再次執行代碼,輸出如下:
Produce a data
Try to consume a data
Hello World!
可以看到,生產的"Hello World!"已經被成功消費并打印到控制臺。
2. 為什么wait方法需要在循環操作中使用?
線程調用wait方法后,可能會出現虛假喚醒(spurious wakeup)的情況,即線程在沒有被notify/notifyAll調用、沒有被中斷、也沒有超時的情況下被喚醒,這是我們不希望發生的情況。
雖然在真實環境中,虛假喚醒的概率非常小,但程序仍然需要在虛假喚醒的情況下保證正確性,因此需要使用while循環結構。
while (條件不滿足) {
obj.wait();
}
這樣,即使線程被虛假喚醒,如果條件不滿足,wait會繼續執行,從而消除虛假喚醒導致的風險。
3.wait/notify和sleep方法有什么異同?
wait方法和sleep方法的相同點如下:
- 它們都可以阻塞線程。
- 它們都可以響應中斷:如果在等待過程中收到中斷信號,它們會響應并拋出InterruptedException異常。
它們之間也有很多不同點:
- wait方法必須在synchronized保護的代碼中使用,而sleep方法沒有這個要求。
- 當sleep方法在synchronized代碼中執行時,它不會釋放鎖,而wait方法會主動釋放鎖。
- sleep方法需要定義一個時間,時間到期后線程會主動恢復。對于沒有參數的wait方法,它意味著永久等待,直到被中斷或喚醒,不會主動恢復。
- wait和notify是Object類的方法,而sleep是Thread類的方法。
好了,這次的內容就到這里,下次再見!