生產者-消費者是多執行緒並發編程中最基礎、最重要的協作模式。本文透過一個「有限倉庫」場景,完整展示使用 wait()/notifyAll() 實現執行緒同步與通訊的標準範式,並分析關鍵細節與現代替代方案。
ProductFactory (倉庫),內部以 char[] 模擬固定容量緩衝區。Producer 執行緒,隨機生成大寫字母放入倉庫,滿則等待。Consumer 執行緒,從倉庫取出字元,空則等待。synchronized 方法 + wait()/notifyAll()。package com.example.concurrency;
import java.util.Random;
public class ProducerConsumerDemo {
public static void main(String[] args) {
ProductFactory factory = new ProductFactory(5); // 倉庫容量 5
new Thread(new Producer(factory), "Producer").start();
new Thread(new Consumer(factory), "Consumer").start();
}
}
/** 共享倉庫:封裝同步邏輯 */
class ProductFactory {
private final char[] buffer; // 環形緩衝區
private int count = 0; // 當前產品數
private int in = 0; // 寫入指標
private int out = 0; // 讀取指標
public ProductFactory(int capacity) {
this.buffer = new char[capacity];
}
/** 生產:滿則 wait,放入後 notifyAll */
public synchronized void produce(char product) throws InterruptedException {
while (count == buffer.length) { // 必須用 while 防虛假喚醒
System.out.println(Thread.currentThread().getName() + " 倉庫滿,等待...");
wait();
}
buffer[in] = product;
in = (in + 1) % buffer.length;
count++;
System.out.printf("%s 生產 '%c' | 倉庫: %s%n",
Thread.currentThread().getName(), product, snapshot());
notifyAll(); // 喚醒消費者
}
/** 消費:空則 wait,取出後 notifyAll */
public synchronized char consume() throws InterruptedException {
while (count == 0) {
System.out.println(Thread.currentThread().getName() + " 倉庫空,等待...");
wait();
}
char product = buffer[out];
out = (out + 1) % buffer.length;
count--;
System.out.printf("%s 消費 '%c' | 倉庫: %s%n",
Thread.currentThread().getName(), product, snapshot());
notifyAll(); // 喚醒生產者
return product;
}
private String snapshot() {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < count; i++) {
int idx = (out + i) % buffer.length;
sb.append(buffer[idx]).append(i == count-1 ? "" : ",");
}
return sb.append("]").toString();
}
}
/** 生產者任務 */
class Producer implements Runnable {
private final ProductFactory factory;
private final Random random = new Random();
public Producer(ProductFactory factory) { this.factory = factory; }
@Override public void run() {
try {
for (int i = 0; i < 20; i++) { // 生產 20 個
char c = (char) ('A' + random.nextInt(26));
factory.produce(c);
Thread.sleep(200); // 模擬生產耗時
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重置中斷狀態
}
}
}
/** 消費者任務 */
class Consumer implements Runnable {
private final ProductFactory factory;
public Consumer(ProductFactory factory) { this.factory = factory; }
@Override public void run() {
try {
for (int i = 0; i < 20; i++) { // 消費 20 個
factory.consume();
Thread.sleep(300); // 模擬消費較慢
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
while 而非 if 檢查條件?// 錯誤寫法
if (count == buffer.length) wait();
// 正確寫法
while (count == buffer.length) wait();
原因:虛假喚醒。執行緒可能在無 notify 的情況下從 wait() 返回 (JVM 實現允許、中斷、系統喚醒)。while 迴圈確保條件滿足後才繼續執行,是「保護性暫停」模式標準寫法。
notify() vs notifyAll()notify():喚醒單一等待執行緒。若喚醒的是同類角色 (如生產者喚醒生產者),條件仍不滿足,導致活鎖或效能浪費。notifyAll():喚醒所有等待執行緒。配合 while 重檢,確保正確角色繼續執行。標準建議:除非極度確定等待集合單一條件,否則一律用 notifyAll()。範例使用 synchronized 實例方法,鎖為 this (ProductFactory 實例)。若同步塊需跨多個物件,必須使用共同的 final 物件 作為鎖:
private final Object lock = new Object();
synchronized(lock) { ... }
原文範例用陣列搬移 (product[flag] = ' '),O(n) 複雜度。環形緩衝區 (Ring Buffer) 以 in/out 指標實現 O(1) 入隊/出隊,生產級庫 (Disruptor, Netty RingBuffer) 均採此結構。
手動 wait/notify 易錯、難維護。生產環境強烈建議使用 java.util.concurrent 高階組件:
| 組件 | 適用場景 | 關鍵方法 |
|---|---|---|
| BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) | 最通用的生產者-消費者通道 | put(e) 阻塞滿、take() 阻塞空 |
| BlockingDeque | 雙端隊列,支援頭尾操作 | putFirst/Last, takeFirst/Last |
| Semaphore | 控制同時存取資源數 (連線池、許可證) | acquire(), release() |
| CountDownLatch / CyclicBarrier | 協調多執行緒起跑/匯合 | await(), countDown() |
BlockingQueue queue = new ArrayBlockingQueue<>(5);
// 生產者
queue.put(c); // 自動阻塞直到有空位
// 消費者
char c = queue.take(); // 自動阻塞直到有元素
優勢:無鎖竊取、公平性可配置、內建阻塞/超時/輪詢 API、完整單元測試、JDK 內建效能優化。
notifyAll():導致執行緒永久飢餓 (Deadlock 的一種)。synchronized 之外修改共享狀態:競態條件。InterruptedException 後未重置中斷旗標 (Thread.currentThread().interrupt()),導致上層無法感知中斷請求。生產者-消費者模式核心在於共享緩衝區 + 互斥鎖 + 條件等待/通知。理解 wait/notify 底層機制有助於面試與閱讀舊代碼,但實戰開發請無腦使用 BlockingQueue,專注於業務邏輯而非同步細節。進階可探索 Disruptor (無鎖環形緩衝區、百萬級吞吐) 或 Reactor/RxJava (反應式流、背壓控制) 解決高併發場景。