生產者-消費者是多線程協作的經典模型。本文通過「倉庫-產品-生產者-消費者」領域模型,展示如何用 synchronized + wait()/notifyAll() 實現線程安全的緩衝區協作,並分析關鍵細節與常見陷阱。
| 角色 | 職責 | 關鍵方法 |
|---|---|---|
Depot(倉庫) | 共享緩衝區,維護容量 capacity 與當前庫存 size | produce(val)、consume(val) |
Producer(生產者) | 持有 Depot 引用,新建線程調用 produce() | produce(val) |
Customer(消費者) | 持有 Depot 引用,新建線程調用 consume() | consume(val) |
class Depot {
private int capacity; // 倉庫容量
private int size; // 當前庫存
public Depot(int capacity) { this.capacity = capacity; this.size = 0; }
public synchronized void produce(int val) {
try {
int left = val; // 剩餘待生產量
while (left > 0) {
// 倉滿等待
while (size >= capacity) wait();
// 計算實際可生產量(不超容)
int inc = (size + left > capacity) ? (capacity - size) : left;
size += inc;
left -= inc;
System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d%n",
Thread.currentThread().getName(), val, left, inc, size);
notifyAll(); // 唤醒消費者
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public synchronized void consume(int val) {
try {
int left = val; // 剩餘待消費量
while (left > 0) {
// 倉空等待
while (size <= 0) wait();
// 計算實際可消費量(不超賣)
int dec = (size < left) ? size : left;
size -= dec;
left -= dec;
System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d%n",
Thread.currentThread().getName(), val, left, dec, size);
notifyAll(); // 唤醒生產者
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
produce/consume 同步於同一 Depot 實例,任一時刻只有一個線程能修改 size,避免競態條件。
while (size >= capacity) wait(); // 正確
// if (size >= capacity) wait(); // 錯誤:虛假喚醒會導致邏輯錯誤
Java 規範允許虛假喚醒,線程可能在無 notify 情況下醒來,必須重新檢查條件。
left 變量追蹤「本次請求剩餘量」,支持單次調用跨越多個等待-生產/消費週期。inc/dec 計算實際增減量,邊界保護:min(剩餘請求, 剩餘容量/庫存)。notify() 可能喚醒同類線程(如生產者喚醒生產者),導致無效切換。notifyAll() 喚醒所有等待線程,由條件判斷自動過濾,雖略增上下文切換但邏輯正確。捕獲後調用 Thread.currentThread().interrupt() 恢復中斷狀態,避免吞噬中斷信號,便於上層協作式取消。
Depot mDepot = new Depot(100);
Producer mPro = new Producer(mDepot);
Customer mCus = new Customer(mDepot);
mPro.produce(60); // Thread-0: 生產 60,size=60
mPro.produce(120); // Thread-4: 生產 40(滿 100),left=70 等待
mCus.consume(90); // Thread-2: 消費 90,size=10,喚醒生產者
mCus.consume(150); // Thread-3: 消費 10(空),left=140 等待
mPro.produce(110); // Thread-1: 生產 90(滿 100),left=20 等待
// ... 交替進行
輸出片段:
Thread-0 produce( 60) --> left= 0, inc= 60, size= 60 Thread-4 produce(120) --> left= 70, inc= 40, size=100 Thread-2 consume( 90) <-- left= 0, dec= 90, size= 10 Thread-3 consume(150) <-- left=140, dec= 10, size= 0 Thread-1 produce(120) --> left= 20, inc=100, size=100 ...
| 陷阱 | 後果 | 修正 |
|---|---|---|
用 if 替代 while | 虛假喚醒導致邏輯錯誤 | 堅持 while (condition) wait() |
忘記 notifyAll() | 線程永久等待(死鎖) | 狀態改變後必須通知 |
wait() 不在 synchronized 中 | 拋 IllegalMonitorStateException | 確保持有監視器鎖 |
吞噬 InterruptedException | 無法響應關閉信號 | 重設中斷標記或拋出 |