NOTE
6.30 Producer-Consumer
Producer-consumer coordination with BlockingQueue, wait/notify, and Condition, including bounded capacity and correct wait-loop semantics.
This is a historical learning note and may contain outdated or incomplete understanding.
1. Prefer BlockingQueue for the Common Case
Producer-consumer is fundamentally a coordination and backpressure problem. A bounded BlockingQueue directly models both:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);
// producer
queue.put(value);
// consumer
Integer value = queue.take();
A bounded queue prevents producers from building an unlimited in-memory backlog when consumers are slower.
SynchronousQueue is a special zero-capacity handoff: every put rendezvous with a take.
2. wait / notifyAll
The lower-level monitor pattern is:
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait();
}
Object value = queue.remove(0);
lock.notifyAll();
}
Always re-check the condition in a while loop because wake-up does not imply that the condition is still true when the thread reacquires the monitor.
For multiple producers/consumers, notifyAll() is usually easier to make correct than trying to infer which single waiter should be awakened.
3. Lock + Condition
A ReentrantLock can expose separate conditions such as notEmpty and notFull. This lets producers wait specifically for capacity and consumers wait specifically for data.
The same rule applies: await in a loop, update shared state while holding the lock, then signal the relevant condition.
4. Choosing the Abstraction
Use BlockingQueue unless you specifically need a custom synchronization protocol. It packages the difficult visibility, locking, waiting, and wake-up rules into a tested higher-level abstraction.