如何避免 Java 中的忙碌等待
1. 簡介
在本教程中,我們將探討線程忙等待的含義。
我們將分析為什麼這種方法並不理想,以及它如何導致 CPU 資源浪費。最後,我們將討論更有效的避免繁忙等待的替代方案。
2.什麼是忙等待?
忙等待是多執行緒系統和作業系統中的一個基本概念。
當執行緒在迴圈中主動檢查某個條件,直到該條件變成true
時,就會發生忙碌等待。這會使線程“卡住”,持續消耗資源,但不做太多工作。我們將使用以下測試用例在實務中檢驗忙等待:
@Test
void givenWorkerThread_whenBusyWaiting_thenAssertExecutedMultipleTimes() {
AtomicBoolean taskDone = new AtomicBoolean(false);
long counter = 0;
Thread worker = new Thread(() -> {
simulateThreadWork();
taskDone.set(true);
});
worker.start();
while (!taskDone.get()) {
counter++;
}
logger.info("Counter: {}", counter);
assertNotEquals(1, counter);
}
我們創建了一個新的工作線程,給它分配了一些工作,然後更新了標誌。執行測試的主執行緒會反覆檢查該標誌,在這種情況下,只要工作執行緒仍然處於活動狀態,它就會處於忙碌等待狀態。
最後,我們可以看到計數器不斷在遞增。這個計數器表示主執行緒在等待期間循環的次數。讓我們觀察控制台並查看它的最終值:
11:14:32.286 [main] INFO cbcbBusyWaitingUnitTest - Counter: 885019109
3.如何避免忙等待?
既然我們已經了解了忙等待的具體實現,我們將討論使用阻塞機制的更有效率方法。與忙等待不同,阻塞機制允許線程暫停執行,直到被明確恢復。
3.1. 傳統方法: wait()
和notify()
最直接的方法之一是使用從Object
類別繼承的傳統內建wait()
和notify()
方法。
這是我們之前範例的一個版本,這次阻塞一個執行緒而不是循環旋轉:
@Test
void givenWorkerThread_whenUsingWaitNotify_thenWaitEfficientlyOnce() {
AtomicBoolean taskDone = new AtomicBoolean(false);
final Object monitor = new Object();
long counter = 0;
Thread worker = new Thread(() -> {
simulateThreadWork();
synchronized (monitor) {
taskDone.set(true);
monitor.notify();
}
});
worker.start();
synchronized (monitor) {
while (!taskDone.get()) {
counter++;
try {
monitor.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Test case failed due to thread interruption!");
}
}
}
assertEquals(1, counter);
}
在此範例中,執行測試的執行緒wait(),
進入 Java 執行緒生命週期中的WAITING
狀態。這意味著該執行緒被掛起,在收到其他執行緒的通知之前不會執行任何工作。
儘管 while 循環可能看起來像是在忙等待,但它有必要處理虛假喚醒。虛假喚醒是指執行緒在沒有收到通知或中斷的情況下恢復等待的情況。因此,通常建議將wait()
放在一個重新檢查條件的循環中。
回顧範例,當工作執行緒完成其任務時,它會設定共享標誌並呼叫notify()
來喚醒等待的執行緒。在測試結束時,我們斷言計數器正好為1
,從而確認該條件僅被有效地檢查了一次。
最後,值得注意的是,許多阻塞機制會引發InterruptedException
異常。如果執行緒在WAITING
狀態下被中斷,方法wait()
會拋出InterruptedException
。我們的範例使用Thread.currentThread().interrupt()
來恢復執行緒的中斷狀態,確保中斷訊號被保留並稍後可以被偵測到。
3.2.現代替代方案
我們使用了wait()
和notify()
方法來示範如何透過基本的執行緒協調來避免忙碌等待。雖然這種方法在某些情況下有效,但值得注意的是,現代的高級並發工具可以簡化同步,並且通常更不容易出錯。
-
CountDownLatch
– 它允許執行緒使用await()
阻塞,直到另一個執行緒透過呼叫countDown()
發出完成訊號,從而消除忙等待。 -
CompletableFuture
– 從設計上避免了忙碌等待,因為它不需要輪詢或主動等待。它非同步運行任務,並在完成後透過非阻塞回調或可選阻塞通知。 -
Lock
與Condition
– 提供比synchronized
區塊更靈活的控制。線程可以等待某個條件,並在就緒時收到訊號,從而避免持續輪詢。
Java 還提供了其他阻塞工具,例如Semaphore
、 CyclicBarrier
和Phaser
用於更高階的協調任務,包括管理有限資源、同步多個執行緒或處理分階段執行。雖然它們更加專業,但它們仍然依賴線程協調來幫助避免忙碌等待。
4. 結論
在本文中,我們探討了多執行緒系統中忙等待的概念。
我們已經了解了忙等待如何浪費寶貴的 CPU 資源,以及為什麼它通常不是一個好的同步策略。透過使用適當的阻塞機制,我們可以避免忙碌等待,並編寫更有效率、更回應的多執行緒程式碼。
與往常一樣,完整的程式碼範例可在 GitHub 上找到。