線程通信核心是「等待-通知」模式:線程等待條件滿足,另一線程滿足條件後通知。Java 提供 Object.wait/notify/notifyAll 及 java.util.concurrent.locks.Condition 兩套機制。本文從共享變量模型進階到條件變量,分析信號丟失、虛假喚醒、鎖釋放等關鍵細節。
class MySignal {
boolean hasDataToProcess = false;
synchronized boolean hasData() { return hasDataToProcess; }
synchronized void setHasData(boolean b) { hasDataToProcess = b; }
}
// 消費者線程
while (!sharedSignal.hasData()) { /* busy wait */ }
缺點:CPU 空轉,無法進入休眠,僅適合極短等待場景。
class MyWaitNotify {
final Object monitor = new Object();
boolean wasSignalled = false;
void doWait() throws InterruptedException {
synchronized (monitor) {
while (!wasSignalled) monitor.wait();
wasSignalled = false; // 清除信號
}
}
void doNotify() {
synchronized (monitor) {
wasSignalled = true;
monitor.notify(); // 或 notifyAll()
}
}
}
wait/notify 必須持有監視器鎖(在 synchronized 塊內)wait() 釋放鎖並進入等待集,被喚醒後重新競爭鎖notify() 隨機喚醒一個等待線程,notifyAll() 喚醒全部場景:通知線程在等待線程調用 wait() 前調用 notify(),信號丟失,等待線程永久阻塞。
解決:引入布爾標誌 wasSignalled 保存信號狀態:
wasSignalled = trueif (!wasSignalled) wait()wasSignalled = false線程可能在無 notify 情況下喚醒(JVM 實現細節)。
對策:必須在 while 循環中調用 wait():
while (!wasSignalled) {
monitor.wait();
}
喚醒後重新檢查條件,不滿足則繼續等待。if 判斷無法應對虛假喚醒。
使用 notifyAll() + while 循環:
wasSignalled=true,執行並清除標誌若用 notify() 可能喚醒非目標線程導致信號實質丟失。
錯誤:使用字符串常量 "" 或全局對象作為鎖:
synchronized ("") { wait(); } // 危險!
原因:字符串常量池導致不同類共享同一鎖對象,引發誤喚醒、信號丟失。
正確:每個協作類使用私有 final Object monitor = new Object()。
ReentrantLock 配合 Condition 提供更強大的等待-通知:
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
void take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
return queue.poll();
} finally { lock.unlock(); }
}
void put(E e) {
lock.lock();
try {
queue.add(e);
notEmpty.signal(); // 或 signalAll()
} finally { lock.unlock(); }
}
優勢:一把鎖可創建多個 Condition(如 notEmpty/notFull),支持公平鎖、可中斷等待、超時等待。
wait/notify 是基礎,Condition 是進階選擇synchronized/Lock 保護下檢查條件並等待BlockingQueue、CountDownLatch、CompletableFuture 等高級組件