為什麼 wait() 需要同步?
一、簡介
在 Java 中,我們有wait()/notify()
API。該API是執行緒間同步的方法之一。為了使用此 API 的方法,目前執行緒必須擁有被呼叫者的監視器。
在本教程中,我們將探討此要求有意義的原因。
2. wait()
的工作原理
首先,我們需要簡單談談 Java 中wait()
的工作原理。在Java中,根據JLS,每個物件都有一個監視器。本質上,這意味著我們可以同步任何我們喜歡的物件。這可能不是一個好的決定,但這就是我們現在所擁有的。
有了這個,當我們呼叫wait()
時,我們隱式地做了兩件事。首先,我們將目前執行緒放入this
物件監視器的 JVM 內部等待集中。第二個是,一旦執行緒處於等待狀態,我們(或 JVM,就此而言)釋放this
物件上的同步鎖。在這裡,我們需要澄清this
一詞表示我們呼叫wait()
方法的物件。
然後,當前執行緒只是在集合中等待,直到另一個執行緒this
物件呼叫notify()
/ notifyAll()
。
3. 為什麼需要監視器所有權?
在上一節中,我們看到JVM所做的第二件事是釋放this
物件上的同步鎖定。為了釋放它,我們顯然需要先擁有它。原因相對簡單: wait()
上的同步是為了避免遺失喚醒問題而提出的要求。這個問題本質上代表了一種情況,即我們有一個等待線程錯過了通知信號。這主要是由於線程之間的競爭條件而發生的。讓我們用一個例子來模擬這個問題。
假設我們有以下 Java 程式碼:
private volatile Boolean jobIsDone;
private Object lock = new Object();
public void ensureCondition() {
while (!jobIsDone) {
try {
lock.wait();
}
catch (InterruptedException e) {
// ...
}
}
}
public void complete() {
jobIsDone = true;
lock.notify();
}
快速說明 - 此程式碼將在運行時失敗並出現IllegalMonitorStateException
。這是因為,在這兩種方法中,我們在wait()
/ notify()
呼叫之前都不會請求lock
物件監視器。因此,此程式碼純粹用於演示和學習目的。
另外,假設我們有兩個線程。因此,線程B
正在做有用的工作。一旦完成,線程B
需要呼叫complete()
方法來發出完成訊號。我們還有另一個線程A,
它正在等待B
執行的作業完成。線程A
透過呼叫ensureCondition()
方法來檢查條件。由於 Linux 核心層級上發生的虛假喚醒問題,對條件的檢查是在循環中進行的,但這是另一個主題。
4. 失去喚醒的問題
讓我們逐步分解我們的範例。假設線程A
調用ensureCondition()
並進入while
循環。它檢查了一個條件,該條件似乎是false,
因此它進入了try
區塊。因為我們是在多執行緒環境下操作,所以另一個執行緒B
可以同時進入complete()
方法。因此, B
可以在執行緒A
呼叫wait()
) 之前呼叫 set 易失性標誌jobIsDone
為true
並呼叫notify()
。
在這種情況下,如果線程B
永遠不會再次進入complete()
,線程A
將永遠等待,因此,與其關聯的所有資源也將永遠存在。如果線程A
碰巧持有另一個鎖,這不僅會導致死鎖,還會導致記憶體洩漏,因為從線程A
堆疊幀可到達的物件將保持活動狀態。這是因為線程A
被認為是活動的,並且它可以恢復執行。因此,GC 不允許對A
堆疊的方法中分配的物件進行垃圾回收。
5、解決方案
因此,為了避免這種情況,我們需要同步。因此,呼叫者在執行之前必須擁有被呼叫者的監視器。因此,讓我們重寫程式碼,考慮同步問題:
private volatile Boolean jobIsDone;
private final Object lock = new Object();
public void ensureCondition() {
synchronized (lock) {
while (!jobIsDone) {
try {
lock.wait();
}
catch (InterruptedException e) {
// ...
}
}
}
}
public void complete() {
synchronized (lock) {
jobIsDone = true;
lock.notify();
}
}
在這裡,我們只是新增了一個synchronized
區塊,在呼叫wait()/notify()
API 之前,我們嘗試在其中取得lock
物件監視器。現在,如果B
在A
呼叫wait()
之前執行complete()
方法,我們可以避免遺失喚醒。這是因為只有當A
還沒有取得lock
物件監視器時, B
才能執行complete()
方法。因此, A
無法在執行complete()
方法時檢查條件。
六,結論
在本文中,我們討論了為什麼 Java wait()
方法需要同步。我們需要被呼叫者監視器的所有權,以避免遺失喚醒異常。如果我們不這樣做,JVM 將採取快速失敗方法並拋出IllegalMonitorStateException
。
與往常一樣,這些範例的原始程式碼可以在 GitHub 上找到。