為什麼 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 上找到。