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

Java wait/notify/notifyAll 深度解析:從原理到生產者消費者模式實戰

     編輯:Java 相關知識

核心前提:三個方法的歸屬與調用契約

  • 定義在 java.lang.Object 中,每個對象天生擁有一把內部鎖和一個等待隊列。
  • 調用前提:當前線程必須持有該對象的鎖(即在 synchronized(obj) 塊或實例同步方法中)。否則拋 IllegalMonitorStateException。
  • 鎖與等待隊列綁定:鎖保護共享狀態,等待隊列管理因條件不滿足而掛起的線程,二者綁定在同一對象上,保證「檢查條件 → 等待/通知」的原子性。

方法語義精準定義

方法行為釋放鎖?喚醒後
wait()當前線程進入對象等待隊列,釋放鎖,阻塞直到被喚醒/中斷/超時✅ 完全釋放重新競爭鎖,獲得後從 wait() 返回
wait(long timeout)帶超時的等待,超時自動喚醒✅同無參版本
notify()喚醒任意一個在該對象等待隊列中的線程❌ 不釋放(需退出同步塊)被喚醒線程進入鎖競爭隊列
notifyAll()喚醒所有在該對象等待隊列中的線程❌同上

為什麼必須在同步塊中調用?(競態條件演示)

經典生產者-消費者場景:

// 錯誤示範:無同步保護
if (buffer.isEmpty()) {          // 1. 消費者檢查條件
    // 上下文切換:生產者執行
    buffer.add(item);            // 2. 生產者添加元素
    buffer.notifyAll();          // 3. 生產者通知(此時消費者還沒 wait!)
    // 切回消費者
    buffer.wait();               // 4. 消費者進入等待 → 永久阻塞!信號丟失
}

結論:「檢查條件」與「等待/通知」必須是原子操作,因此必須持有鎖:

synchronized (buffer) {          // 持有鎖
    while (buffer.isEmpty()) {   // 條件不滿足
        buffer.wait();           // 釋放鎖並等待,原子性完成
    }
    // 條件滿足,繼續執行,仍持有鎖
    Item item = buffer.take();
}

經典模式:單槽位緩衝區(生產者-消費者)

public class SingleSlotBuffer {
    private int value;
    private boolean hasValue = false;

    /** 消費者取走數據 */
    public synchronized int take() {
        while (!hasValue) {               // while 防虛假喚醒
            try { wait(); }               // 釋放鎖等待
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }
        hasValue = false;
        notify();                         // 喚醒生產者
        return value;
    }

    /** 生產者放入數據 */
    public synchronized void put(int value) {
        while (hasValue) {
            try { wait(); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }
        this.value = value;
        hasValue = true;
        notify();                         // 喚醒消費者
    }
}

線程類實現

public class Producer implements Runnable {
    private final SingleSlotBuffer buffer;
    public Producer(SingleSlotBuffer buffer) { this.buffer = buffer; }
    @Override public void run() {
        for (int i = 0; ; i++) {
            buffer.put(i);
            System.out.println("Produced: " + i);
            sleepRandom(100, 500);
        }
    }
    private void sleepRandom(int min, int max) {
        try { Thread.sleep((long)(Math.random()*(max-min+1)+min)); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

public class Consumer implements Runnable {
    private final SingleSlotBuffer buffer;
    public Consumer(SingleSlotBuffer buffer) { this.buffer = buffer; }
    @Override public void run() {
        while (true) {
            int val = buffer.take();
            System.out.println("Consumed: " + val);
            sleepRandom(100, 500);
        }
    }
    private void sleepRandom(int min, int max) {
        try { Thread.sleep((long)(Math.random()*(max-min+1)+min)); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

public class Main {
    public static void main(String[] args) {
        SingleSlotBuffer buffer = new SingleSlotBuffer();
        new Thread(new Producer(buffer), "Producer").start();
        new Thread(new Consumer(buffer), "Consumer").start();
    }
}

進階題目:兩線程交替打印 1,2,1,2…

public class AlternatePrinter {
    private static final Object LOCK = new Object();
    private static int next = 1; // 1 或 2

    public static void main(String[] args) {
        new Thread(() -> printNum(1), "T1").start();
        new Thread(() -> printNum(2), "T2").start();
    }

    static void printNum(int num) {
        while (true) {
            synchronized (LOCK) {
                while (next != num) {   // 不是自己輪次 → 等待
                    try { LOCK.wait(); }
                    catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
                }
                System.out.println(Thread.currentThread().getName() + ": " + num);
                next = (num == 1) ? 2 : 1; // 切換輪次
                LOCK.notifyAll();          // 喚醒另一線程
            }
        }
    }
}

關鍵點:while 循環檢查條件(防虛假喚醒)、執行完業務邏輯先修改共享狀態再 notifyAll、使用同一把鎖 LOCK。

虛假喚醒與 while 循環必須性

JVM 規範允許線程在無 notify、無中斷、無超時情況下自發從 wait() 返回(虛假喚醒)。因此必須用 while (conditionNotMet) wait(); 而非 if,醒來後重新檢查條件,不滿足繼續等待。

wait/notify vs 現代并發工具對比

維度Object.wait/notifyjava.util.concurrent (Lock/Condition)
API 層級底層、面向對象鎖高層、顯式鎖/條件變量
多條件支持一個對象一個等待隊列一個 Lock 可綁定多個 Condition
可中斷等待不支持(只能中斷線程)condition.awaitInterruptibly()
公平性控制無ReentrantLock(true) 公平鎖
超時精度毫秒/納秒納秒,支持 TimeUnit
編程複雜度低(隱式鎖)中(顯式鎖/解鎖)
推薦場景簡單同步、教學演示生產級複雜同步邏輯

最佳實踐清單

  1. 永遠在 synchronized 塊中調用 wait/notify/notifyAll。
  2. 永遠用 while 循環檢查條件,防虛假喚醒。
  3. 先修改共享狀態,再 notify/notifyAll,確保被喚醒線程看到最新數據。
  4. 優先用 notifyAll,除非確定只有一類等待線程且單喚醒無遺漏風險。
  5. 復雜場景遷移到 Lock/Condition,獲得更精確控制。
  6. 生產代碼優先用高級組件:BlockingQueue、Semaphore、CountDownLatch、CompletableFuture 等,減少手動同步代碼。

總結

wait/notify/notifyAll 是 Java 并發的基石,理解「鎖保護狀態、等待隊列管理線程、條件檢查與等待原子化」三位一體的設計,能寫出正確的生產者-消費者、交替打印等經典模式。實戰中,簡單場景可用,復雜場景請擁抱 java.util.concurrent。

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