首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 多線程 notify 與 notifyAll 核心差異與死鎖風險深度分析

     編輯:Java 相關知識

前言

Object.wait()、notify()、notifyAll() 是 Java 線程協作的基礎原語。本文深度剖析二者的語義差異、喚醒機制、以及 notify() 易導致「信號丟失」與死鎖的經典場景,指導正確選型。

一、語義與機制對比

方法喚醒數量適用場景風險
notify()隨機喚醒 一個 在該對象監視器上等待的線程所有等待線程執行同質任務,任何線程處理效果等價異質任務場景極易丟失信號,導致部分鍵線程永久等待
notifyAll()喚醒 所有 在該對象監視器上等待的線程等待線程執行異質任務(如生產者/消費者混合)、條件判斷複雜喚醒風暴(Thundering Herd),短暫性能抖動

關鍵共同點

  • 喚醒後的線程不直接運行,進入鎖競爭隊列(Entry Set),需重新獲取監視器鎖
  • 同一時刻僅有一個線程持有鎖並執行同步塊代碼
  • 被喚醒線程需在 while(condition) wait() 循環中重新檢查條件(防止虛假喚醒)

二、經典死鎖案例:生產者-消費者模型

錯誤代碼(使用 notify)

public class Buffer {
    private final List buf = new ArrayList<>();
    private final int MAX_SIZE = 10;

    public synchronized void put(Object o) {
        while (buf.size() == MAX_SIZE) {
            wait(); // 滿了等待
        }
        buf.add(o);
        notify(); // 唤醒一個線程
    }

    public synchronized Object get() {
        while (buf.size() == 0) {
            wait(); // 空了等待
        }
        Object o = buf.remove(0);
        notify(); // 唤醒一個線程
        return o;
    }
}


死鎖演練過程

  1. 緩衝區滿,生產者 P1 調用 put() → wait() 釋放鎖,進入 Wait Set
  2. 消費者 C1 調用 get() 取走元素 → notify() 隨機喚醒
  3. 不幸喚醒了另一個消費者 C2(而非生產者)
  4. C2 獲取鎖 → 檢查 buf.size() == 0 → 仍為空 → 再次 wait()
  5. 生產者 P1 永遠在 Wait Set 中沉睡,無人喚醒 → 系統死鎖

根因

notify() 無法指定喚醒「生產者」還是「消費者」。當等待集合包含不同條件的線程時,隨機喚醒極大概率喚醒「不滿足條件」的線程,導致有效信號丟失。

三、正確寫法:notifyAll 保證活性

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;
}

雖喚醒所有線程引發短暫競爭,但保證至少一個正確類型的線程被喚醒並推進系統狀態,從根本上避免信號丟失死鎖。

四、何時可以安全使用 notify?

僅當所有等待線程同質同構時:

  • 線程池工作者線程等待任務隊列(taskQueue.wait())
  • 所有線程執行完全相同的邏輯,任意線程被喚醒效果等價
  • 無「生產者/消費者」等角色區分

即使如此,notifyAll() 仍是更安全的默認選擇,性能差異在現代 JVM 中可忽略。

五、現代替代方案:java.util.concurrent

自 JDK 5 起,強烈建議使用高層併發工具而非手動 wait/notify:

場景推薦類核心優勢
生產者-消費者BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue)內置阻塞 put/take、容量控制、線程安全、無需手動同步
條件等待Condition (Lock.newCondition())一把鎖支持多個條件變量、精確喚醒、可中斷等待
信號量控制Semaphore許可證模型、公平/非公平、批量獲取釋放
倒計時/屏障CountDownLatch, CyclicBarrier線程協作同步、可重用/不可重用
完成通知CompletableFuture異步編程、組合式回調、異常傳播

BlockingQueue 示例

BlockingQueue queue = new LinkedBlockingQueue<>(100);

// 生產者
queue.put(task); // 滿則阻塞

// 消費者
Task task = queue.take(); // 空則阻塞

零同步代碼,零 wait/notify,零死鎖風險。

六、最佳實踐總結

  1. 默認使用 notifyAll(),除非經嚴格證明所有等待者同質
  2. 優先使用 java.util.concurrent 組件,將同步協作委託給專家級庫
  3. 若必須手寫 wait/notify:
      ✓ 總在 while(condition) wait() 循環中
      ✓ 持有鎖調用
      ✓ 狀態變更後調用 notifyAll()
      ✓ 文檔說明等待條件與喚醒邏輯
  4. 避免在公共對象(如 String、Integer、this 可能被外部同步)上 wait/notify

結語

notify() 是「賭博」,賭喚醒的那個線程恰好是能推進系統狀態的;notifyAll() 是「廣播」,確保所有潛在推進者都有機會競爭。在異質協作場景(生產者/消費者、讀者/寫者、不同業務條件)中,notify() 幾乎必然導致活性失效。現代 Java 開發應將 wait/notify 視為「組裝語言」,拥抱 BlockingQueue、Condition、CompletableFuture 等高級抽象,將精力聚焦於業務邏輯而非同步原語的微操之上。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved