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

Java 多線程核心:wait/notify/notifyAll 機制深度解析與實戰陷阱

     編輯:Java 相關知識

前言

wait()、notify()、notifyAll() 是 Java 線程間通信的基石,但也是並發 Bug 的高發區。本文通過源碼級語義分析、實戰代碼演示、輸出對比,深度揭示三者差異、喚醒機制、虛假喚醒處理及生產環境最佳實踐。

一、 核心語義速覽

方法所屬類前置條件語義鎖行為
wait()Object持有對象監視器鎖釋放鎖,進入等待集,阻塞直到被喚醒或超時釋放當前對象鎖
notify()Object持有對象監視器鎖隨機喚醒等待集中一個線程不釋放鎖,退出同步塊後釋放
notifyAll()Object持有對象監視器鎖喚醒等待集中所有線程不釋放鎖,退出同步塊後釋放

二、 關鍵機制深度剖析

1. 等待集與同步隊列

每個對象關聯兩個隊列:

  • 同步隊列:競爭鎖的線程排隊處。
  • 等待集:調用 wait() 釋放鎖後進入的集合。

notify/notifyAll 將線程從等待集移至同步隊列,線程需重新競爭鎖才能繼續執行。

2. 虛假喚醒與循環等待模式

JVM 規範允許線程在無 notify、無中斷、無超時情況下自發醒來(虛假喚醒)。必須在循環中調用 wait(),重新檢查條件:

synchronized (lock) {
    while (!condition) {  // 必須用 while,不可用 if
        lock.wait();
    }
    // 條件滿足,執行業務邏輯
}

3. notify vs notifyAll:經典生產者-消費者陷阱

場景:一個緩衝區,多生產者、多消費者。若使用 notify():

  • 生產者生產後 notify(),可能喚醒另一個生產者(緩衝區已滿),導致其再次 wait(),真正的消費者永不被喚醒 → 信號丟失,系統假死。
  • notifyAll() 喚醒所有,消費者檢查條件消費,生產者檢查條件繼續等待,正確推進。

原則:除非確定等待集中所有線程等待同一條件、且每次喚醒只需一個線程響應(如線程池任務分發),否則一律使用 notifyAll()。

三、 實戰代碼演示:notify 導致的線程飢餓

1. 測試類設計

public class WaitNotifyDemo {
    private int counter = 0;
    private final String name;

    public WaitNotifyDemo(int counter, String name) {
        this.counter = counter;
        this.name = name;
    }

    public synchronized void doSomething() {
        int temp = --counter;
        if (temp <= 0) {
            customizedNotifyAll(); // 或 customizedNotify()
        } else {
            while (temp > 0) {
                try {
                    System.out.println(Thread.currentThread().getName() + "-<" + name + temp + "> will invoke WAIT()");
                    --temp;
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    notifyAll(); // 中斷時保護性喚醒
                }
                System.out.println(Thread.currentThread().getName() + "-<" + name + temp + "> has been ACTIVED");
            }
            customizedNotifyAll();
        }
    }

    public void customizedNotifyAll() {
        notifyAll(); // 此處可替換為 notify() 觀察差異
        System.out.println(Thread.currentThread().getName() + "-<" + name + counter + ">::INVOKED NOTIFYALL() AND FINISHED");
    }
}

2. 運行器

public class TestThread implements Runnable {
    private final WaitNotifyDemo demo;
    public TestThread(WaitNotifyDemo demo) { this.demo = demo; }
    public void run() { demo.doSomething(); }

    public static void main(String[] args) {
        WaitNotifyDemo demo = new WaitNotifyDemo(4, "DAVID");
        new Thread(new TestThread(demo)).start();
        new Thread(new TestThread(demo)).start();
        new Thread(new TestThread(demo)).start();
        new Thread(new TestThread(demo)).start();
    }
}

3. 使用 notifyAll() 輸出(正常推進)

Thread-0-<DAVID3> will invoke WAIT()
Thread-1-<DAVID2> will invoke WAIT()
Thread-2-<DAVID1> will invoke WAIT()
Thread-3-<DAVID0>::INVOKED NOTIFYALL() AND FINISHED
Thread-0-<DAVID2> has been ACTIVED
Thread-0-<DAVID2> will invoke WAIT()
Thread-1-<DAVID1> has been ACTIVED
Thread-1-<DAVID1> will invoke WAIT()
Thread-2-<DAVID0> has been ACTIVED
Thread-2-<DAVID0>::INVOKED NOTIFYALL() AND FINISHED
Thread-0-<DAVID1> has been ACTIVED
Thread-0-<DAVID1> will invoke WAIT()
Thread-1-<DAVID0> has been ACTIVED
Thread-1-<DAVID0>::INVOKED NOTIFYALL() AND FINISHED
Thread-0-<DAVID0> has been ACTIVED
Thread-0-<DAVID0>::INVOKED NOTIFYALL() AND FINISHED

所有線程依次喚醒、遞減計數、最終全部結束。

4. 使用 notify() 輸出(線程飢餓/假死)

Thread-1-<DAVID3> will invoke WAIT()
Thread-0-<DAVID2> will invoke WAIT()
Thread-2-<DAVID1> will invoke WAIT()
Thread-3-<DAVID0>::INVOKED NOTIFY() AND FINISHED
Thread-1-<DAVID2> has been ACTIVED
Thread-1-<DAVID2> will invoke WAIT()
...(後續無輸出,程序掛起)

分析:notify() 隨機喚醒一個等待線程,大概率喚醒的是仍需等待的線程(而非已滿足條件的),導致已滿足條件的線程永久留在等待集,系統停滯。

四、 生產環境最佳實踐

  1. 首選 notifyAll():除非極其確定單條件單響應場景。
  2. 循環等待 + 條件檢查:while (!condition) wait(); 防虛假喚醒。
  3. 釋放鎖前完成狀態變更:修改共享狀態後再調用 notifyAll(),確保被喚醒線程見到最新狀態。
  4. 優先使用高級併發工具:CountDownLatch、CyclicBarrier、Semaphore、BlockingQueue、Lock/Condition 等,語義更清晰、不易出錯。
  5. Condition 替代 Object 監視器:Lock 支持多條件變量(newCondition()),精準喚醒特定條件等待線程,避免驚群效應。

五、 總結

wait/notify/notifyAll 是基於對象監視器的底層協作機制,notify() 隨機單喚醒極易導致信號丟失與線程飢餓,生產代碼中強烈建議使用 notifyAll() 或更高級的併發組件。理解等待集、同步隊列、虛假喚醒、鎖釋放時機,是編寫正確並發程序的前提。

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