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

Java 多線程深度解析:線程通訊機制與實戰模式

     編輯:Java 相關知識

前言:為什麼需要線程通訊?

在多線程程式設計中,線程之間並非孤立運作。為了完成複雜的任務,線程往往需要相互協作、共享數據或同步執行進度。這就引出了線程通訊的核心目的:讓線程能夠發送信號,也能等待其他線程的信號。無論是生產者-消費者模型,還是複雜的狀態機流轉,線程通訊都是構建高併發系統的基石。

核心通訊方式總覽

  1. 共享對象通訊:透過共享變量傳遞狀態
  2. 忙等待:輪詢檢查條件,消耗 CPU 資源
  3. wait()/notify()/notifyAll():Java 內建的高效等待/通知機制
  4. 信號丟失問題:notify 在無線程等待時丟失信號
  5. 虛假喚醒:線程在無 notify 情況下意外醒來
  6. 多線程等待同一信號:需配合循環檢查條件
  7. 避免在常量字串/全域物件上 wait():防止鎖對象被意外共享

一、共享對象通訊:最直觀但需謹慎的起點

最基本的通訊方式是在共享對象中設置標誌位。例如:

public class MySignal {
    private boolean hasDataToProcess = false;

    public synchronized boolean hasDataToProcess() {
        return this.hasDataToProcess;
    }

    public synchronized void setHasDataToProcess(boolean hasData) {
        this.hasDataToProcess = hasData;
    }
}

關鍵點:線程 A 和 B 必須持有同一個 MySignal 實例的引用。若各自持有不同實例,則無法感知對方的信號變化。實際數據可存放於獨立的共享緩衝區,與信號對象分離存儲。

二、忙等待:簡單但低效的輪詢

protected MySignal sharedSignal = ...;
while (!sharedSignal.hasDataToProcess()) {
    // 空循環,佔用 CPU
}

缺點明顯:白白浪費 CPU 週期。除非平均等待時間極短,否則應避免使用,改用阻塞式等待機制。

三、wait()/notify()/notifyAll():Java 內建的高效協作機制

Java 透過 java.lang.Object 定義三個方法實現線程協作:

  • wait():釋放鎖並進入等待狀態,直到被喚醒
  • notify():隨機喚醒一個在該對象上等待的線程
  • notifyAll():喚醒所有在該對象上等待的線程

強制要求:調用這三個方法時,線程必須持有該對象的鎖(即在 synchronized 區塊內調用),否則拋出 IllegalMonitorStateException。

核心機制:wait() 會釋放鎖

這是許多初學者困惑的地方:當線程調用 wait() 時,它會釋放持有的監視器鎖,從而允許其他線程進入同步區塊調用 notify()。被喚醒的線程需重新獲取鎖才能從 wait() 返回繼續執行。

標準範例:MyWaitNotify

public class MyWaitNotify {
    private final Object monitorObject = new Object();
    private boolean wasSignalled = false;

    public void doWait() throws InterruptedException {
        synchronized (monitorObject) {
            while (!wasSignalled) {
                monitorObject.wait();
            }
            wasSignalled = false; // 清除信號
        }
    }

    public void doNotify() {
        synchronized (monitorObject) {
            wasSignalled = true;
            monitorObject.notify();
        }
    }
}

四、信號丟失與解決方案

問題:notify()/notifyAll() 不保存信號。若通知發生在等待線程調用 wait() 之前,信號將丟失,導致線程永久等待。

解決:在共享類中以成員變量保存信號狀態(如上例的 wasSignalled)。通知前先置 true,等待前檢查該標誌,若已有信號則直接處理而不調用 wait()。

五、虛假喚醒:防禦性編程的必要性

線程可能在無 notify 情況下意外醒來(JVM 實現細節、系統中斷等)。若直接繼續執行會導致嚴重邏輯錯誤。

標準對策:使用 while 循環而非 if 判斷條件:

while (!wasSignalled) {
    monitorObject.wait();
}

若發生虛假喚醒,wasSignalled 仍為 false,循環將再次進入等待。此模式稱為「自旋鎖」風格檢查,雖消耗少量 CPU,但保證正確性。

六、多線程等待同一信號:while 循環的又一優勢

當多線程被 notifyAll() 喚醒,只有一個能獲取鎖並清除標誌(wasSignalled = false)。其他線程醒來後檢查標誌發現已被清除,會重新進入等待,直到下一次真正的信號到來。這完美解決了「驚群效應」下的信號搶佔問題。

七、嚴禁在字串常量或全域物件上 wait()/notify()

陷阱:JVM 會將相同字面值的字串常量合併為同一物件(字串常量池)。若多個類實例使用 "" 或 "LOCK" 作為鎖,實際上共享同一把鎖。

後果:

  • 線程 A 等待實例 1 的鎖,線程 B 通知實例 2 的鎖 → 實際操作同一把鎖
  • 若使用 notify() 而非 notifyAll(),可能喚醒錯誤線程導致信號實質丟失
  • 即使用 notifyAll() 也會造成不必要的喚醒開銷

最佳實踐:每個協作類持有獨立的私有鎖對象(如 private final Object monitor = new Object();),絕不使用字串常量、Boolean、Integer 等全域緩存物件作為鎖。

總結:構建健壯線程通訊的關鍵原則

  1. 優先使用 wait()/notify() 替代忙等待
  2. 始終在 synchronized 區塊內調用等待/通知方法
  3. 用成員變量保存信號狀態,防止信號丟失
  4. 用 while 循環 檢查條件,防禦虛假喚醒
  5. 使用獨立、私有、非全域的鎖對象
  6. 在 finally 區塊中釋放鎖(雖然 wait() 會自動釋放,但業務邏輯鎖需手動管理)

掌握這些核心機制與最佳實踐,即可編寫出高效、安全、可維護的多線程協作代碼。後續將補充 java.util.concurrent 套件中的高階工具(如 CountDownLatch、CyclicBarrier、Phaser 等)的實戰應用。

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