Object.wait()、notify()、notifyAll() 是 Java 線程協作的基礎原語。本文深度剖析二者的語義差異、喚醒機制、以及 notify() 易導致「信號丟失」與死鎖的經典場景,指導正確選型。
| 方法 | 喚醒數量 | 適用場景 | 風險 |
|---|---|---|---|
notify() | 隨機喚醒 一個 在該對象監視器上等待的線程 | 所有等待線程執行同質任務,任何線程處理效果等價 | 異質任務場景極易丟失信號,導致部分鍵線程永久等待 |
notifyAll() | 喚醒 所有 在該對象監視器上等待的線程 | 等待線程執行異質任務(如生產者/消費者混合)、條件判斷複雜 | 喚醒風暴(Thundering Herd),短暫性能抖動 |
while(condition) wait() 循環中重新檢查條件(防止虛假喚醒)public class Buffer {
private final List
put() → wait() 釋放鎖,進入 Wait Setget() 取走元素 → notify() 隨機喚醒buf.size() == 0 → 仍為空 → 再次 wait()notify() 無法指定喚醒「生產者」還是「消費者」。當等待集合包含不同條件的線程時,隨機喚醒極大概率喚醒「不滿足條件」的線程,導致有效信號丟失。
public synchronized void put(Object o) {
while (buf.size() == MAX_SIZE) wait();
buf.add(o);
notifyAll(); // 唤醒所有:生產者檢查滿、消費者檢查空,各自決定是否繼續等待
}
public synchronized Object get() {
while (buf.size() == 0) wait();
Object o = buf.remove(0);
notifyAll(); // 同理
return o;
}
雖喚醒所有線程引發短暫競爭,但保證至少一個正確類型的線程被喚醒並推進系統狀態,從根本上避免信號丟失死鎖。
僅當所有等待線程同質同構時:
taskQueue.wait())即使如此,notifyAll() 仍是更安全的默認選擇,性能差異在現代 JVM 中可忽略。
自 JDK 5 起,強烈建議使用高層併發工具而非手動 wait/notify:
| 場景 | 推薦類 | 核心優勢 |
|---|---|---|
| 生產者-消費者 | BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) | 內置阻塞 put/take、容量控制、線程安全、無需手動同步 |
| 條件等待 | Condition (Lock.newCondition()) | 一把鎖支持多個條件變量、精確喚醒、可中斷等待 |
| 信號量控制 | Semaphore | 許可證模型、公平/非公平、批量獲取釋放 |
| 倒計時/屏障 | CountDownLatch, CyclicBarrier | 線程協作同步、可重用/不可重用 |
| 完成通知 | CompletableFuture | 異步編程、組合式回調、異常傳播 |
BlockingQueue queue = new LinkedBlockingQueue<>(100);
// 生產者
queue.put(task); // 滿則阻塞
// 消費者
Task task = queue.take(); // 空則阻塞
零同步代碼,零 wait/notify,零死鎖風險。
while(condition) wait() 循環中String、Integer、this 可能被外部同步)上 wait/notifynotify() 是「賭博」,賭喚醒的那個線程恰好是能推進系統狀態的;notifyAll() 是「廣播」,確保所有潛在推進者都有機會競爭。在異質協作場景(生產者/消費者、讀者/寫者、不同業務條件)中,notify() 幾乎必然導致活性失效。現代 Java 開發應將 wait/notify 視為「組裝語言」,拥抱 BlockingQueue、Condition、CompletableFuture 等高級抽象,將精力聚焦於業務邏輯而非同步原語的微操之上。