Producer-Consumer Problem
IntermediateThe producer-consumer problem models threads that produce and consume items through a shared bounded buffer, requiring synchronization so producers wait when the buffer is full and consumers wait when it is empty.
Overview
The producer-consumer problem (also called the bounded-buffer problem) is the most common concurrency pattern. One or more producer threads generate data items and place them in a shared buffer. One or more consumer threads remove items and process them. The buffer has a fixed capacity. Producers must block when the buffer is full and consumers must block when it is empty. Without synchronization: race conditions corrupt the buffer. Without blocking: busy-waiting wastes CPU. Three Java solutions exist, in increasing abstraction: (1) synchronized + wait/notifyAll — classic, educational; (2) ReentrantLock + Condition — named conditions, better throughput; (3) BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) — the idiomatic Java solution that handles all synchronization internally. Java's java.util.concurrent package exists largely to solve producer-consumer correctly.
Solution 1: synchronized + wait/notifyAll
The foundational approach uses Java's intrinsic monitor. Producers call put() which waits while the buffer is full; consumers call take() which waits while empty. notifyAll() wakes all blocked threads after every state change. This is correct but has thundering herd issues — every state change wakes all threads.
// Producer-Consumer: synchronized + wait/notifyAll
class SharedBuffer {
private final int[] buffer;
private int count = 0, in = 0, out = 0;
SharedBuffer(int size) { buffer = new int[size]; }
public synchronized void produce(int item) throws InterruptedException {
while (count == buffer.length) wait(); // full → block
buffer[in] = item;
in = (in + 1) % buffer.length;
count++;
notifyAll(); // wake sleeping consumers
}
public synchronized int consume() throws InterruptedException {
while (count == 0) wait(); // empty → block
int item = buffer[out];
out = (out + 1) % buffer.length;
count--;
notifyAll(); // wake sleeping producers
return item;
}
}
SharedBuffer buf = new SharedBuffer(5);
Thread producer = new Thread(() -> {
for (int i = 1; i <= 20; i++) {
try { buf.produce(i); System.out.println("Produced: " + i); }
catch (InterruptedException e) { break; }
}
});
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 20; i++) {
try { System.out.println("Consumed: " + buf.consume()); }
catch (InterruptedException e) { break; }
}
});
producer.start(); consumer.start();
producer.join(); consumer.join();Solution 3: BlockingQueue (Idiomatic Java)
ArrayBlockingQueue is the production Java solution. put() blocks when full; take() blocks when empty — internally using ReentrantLock + two Conditions. No explicit synchronization needed in application code. This is the correct solution for real applications: thread-safe, performant, and handles all edge cases.
// Producer-Consumer: BlockingQueue — idiomatic, production-ready Java
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
// Producer thread
Thread producer = Thread.ofVirtual().name("producer").start(() -> {
for (int i = 1; i <= 20; i++) {
try {
queue.put(i); // blocks automatically when full
System.out.println("Produced: " + i + " queue=" + queue.size());
} catch (InterruptedException e) { break; }
}
// Poison pill: signal consumer to stop
try { queue.put(-1); } catch (InterruptedException ignored) {}
});
// Consumer thread
Thread consumer = Thread.ofVirtual().name("consumer").start(() -> {
while (true) {
try {
int item = queue.take(); // blocks automatically when empty
if (item == -1) break; // poison pill received
System.out.println("Consumed: " + item);
} catch (InterruptedException e) { break; }
}
});
producer.join(); consumer.join();
// Other BlockingQueue implementations:
// LinkedBlockingQueue: optionally bounded, higher throughput (two lock design)
// PriorityBlockingQueue: unbounded, ordered by priority
// SynchronousQueue: zero-capacity, direct handoff (used by CachedThreadPool)
// DelayQueue: elements available only after a delay (scheduled tasks)Key Points to Remember
- 1Producer-consumer requires: mutual exclusion on buffer state, blocking when full (producers), blocking when empty (consumers).
- 2synchronized + wait/notifyAll: correct but notifyAll() causes thundering herd on every state change.
- 3ReentrantLock + two Conditions: targeted signal() — notFull wakes producers, notEmpty wakes consumers.
- 4BlockingQueue (ArrayBlockingQueue): idiomatic Java solution — handles all synchronization internally.
- 5Poison pill: sentinel value placed by producer to signal consumers to shut down gracefully.
- 6SynchronousQueue: zero-capacity queue for direct handoff — each put() blocks until a take() is ready.
Interview Questions
Sign in to ask AriaImplement producer-consumer in Java using synchronized and wait/notifyAll.
What is a thundering herd and how do named Conditions in ReentrantLock solve it?
What is a poison pill and how is it used to gracefully shut down consumer threads?
What is the difference between ArrayBlockingQueue and LinkedBlockingQueue?
Ask Aria about Producer-Consumer Problem
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.