在多線程程式設計中,線程之間並非孤立運作。為了完成複雜的任務,線程往往需要相互協作、共享數據或同步執行進度。這就引出了線程通訊的核心目的:讓線程能夠發送信號,也能等待其他線程的信號。無論是生產者-消費者模型,還是複雜的狀態機流轉,線程通訊都是構建高併發系統的基石。
最基本的通訊方式是在共享對象中設置標誌位。例如:
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 週期。除非平均等待時間極短,否則應避免使用,改用阻塞式等待機制。
Java 透過 java.lang.Object 定義三個方法實現線程協作:
wait():釋放鎖並進入等待狀態,直到被喚醒notify():隨機喚醒一個在該對象上等待的線程notifyAll():喚醒所有在該對象上等待的線程強制要求:調用這三個方法時,線程必須持有該對象的鎖(即在 synchronized 區塊內調用),否則拋出 IllegalMonitorStateException。
這是許多初學者困惑的地方:當線程調用 wait() 時,它會釋放持有的監視器鎖,從而允許其他線程進入同步區塊調用 notify()。被喚醒的線程需重新獲取鎖才能從 wait() 返回繼續執行。
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,但保證正確性。
當多線程被 notifyAll() 喚醒,只有一個能獲取鎖並清除標誌(wasSignalled = false)。其他線程醒來後檢查標誌發現已被清除,會重新進入等待,直到下一次真正的信號到來。這完美解決了「驚群效應」下的信號搶佔問題。
陷阱:JVM 會將相同字面值的字串常量合併為同一物件(字串常量池)。若多個類實例使用 "" 或 "LOCK" 作為鎖,實際上共享同一把鎖。
後果:
notify() 而非 notifyAll(),可能喚醒錯誤線程導致信號實質丟失notifyAll() 也會造成不必要的喚醒開銷最佳實踐:每個協作類持有獨立的私有鎖對象(如 private final Object monitor = new Object();),絕不使用字串常量、Boolean、Integer 等全域緩存物件作為鎖。
wait()/notify() 替代忙等待synchronized 區塊內調用等待/通知方法掌握這些核心機制與最佳實踐,即可編寫出高效、安全、可維護的多線程協作代碼。後續將補充 java.util.concurrent 套件中的高階工具(如 CountDownLatch、CyclicBarrier、Phaser 等)的實戰應用。