Producer-Consumer Problem in Java
The producer-consumer problem is a classic concurrency problem. Producer threads create items and put them into a shared buffer of limited size; consumer threads take items out and process them. Producers must wait when the buffer is full, and consumers must wait when it is empty. A print queue, an order-processing system and a video player's frame buffer all work this way. In this lesson, you will solve the problem first with wait and notifyAll, and then with Java's ready-made BlockingQueue.
The Rules
| Situation | What must happen |
|---|---|
| Buffer is full | Producers wait until a consumer removes an item |
| Buffer is empty | Consumers wait until a producer adds an item |
| An item is added or removed | Waiting threads are notified |
Example: A Bounded Buffer with wait and notifyAll
Let's see an example with one producer, one consumer and a buffer of two slots. The consumer is deliberately slower, so the buffer fills up and the producer has to wait.
import java.util.ArrayDeque;
import java.util.Queue;
public class BoundedBuffer {
private final Queue<String> items = new ArrayDeque<>();
private final int capacity;
int producerWaits = 0;
BoundedBuffer(int capacity) {
this.capacity = capacity;
}
synchronized void put(String item) throws InterruptedException {
while (items.size() == capacity) {
producerWaits++;
wait();
}
items.add(item);
notifyAll();
}
synchronized String take() throws InterruptedException {
while (items.isEmpty()) {
wait();
}
String item = items.remove();
notifyAll();
return item;
}
public static void main(String[] args) throws InterruptedException {
BoundedBuffer buffer = new BoundedBuffer(2);
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 6; i++) {
buffer.put("order-" + i);
}
buffer.put("DONE");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
String item;
while (!(item = buffer.take()).equals("DONE")) {
Thread.sleep(50);
System.out.println("Srihitha packed " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("Producer had to wait: "
+ (buffer.producerWaits > 0));
}
}Srihitha packed order-1
Srihitha packed order-2
Srihitha packed order-3
Srihitha packed order-4
Srihitha packed order-5
Srihitha packed order-6
Producer had to wait: trueExplanation: put waits in a loop while the buffer is full; take waits while it is empty. After every change, notifyAll wakes the other side. Because the consumer takes 50 milliseconds per order, the two-slot buffer fills and the producer waits. Orders are always packed in the order they were produced, and the special "DONE" item tells the consumer to stop.
Example: The Same Thing with BlockingQueue
java.util.concurrent.BlockingQueue already does all of this. put waits when the queue is full and take waits when it is empty:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class QueueVersion {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
Thread producer = new Thread(() -> {
try {
String[] names = {"Navika", "Srihitha", "Advitha"};
for (String name : names) {
queue.put("ticket for " + name);
}
queue.put("DONE");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
String t;
while (!(t = queue.take()).equals("DONE")) {
System.out.println("Printed " + t);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}Printed ticket for Navika
Printed ticket for Srihitha
Printed ticket for AdvithaExplanation: ArrayBlockingQueue with capacity 2 handles the waiting and signaling internally, so the code has no synchronized, wait or notifyAll at all.
Common Mistakes
Using if instead of while before wait(). With several producers or consumers, a woken thread may find the buffer full or empty again.
Using notify() with several producers and consumers. It may wake a thread of the wrong kind, and everyone ends up waiting.
No way to stop the consumer. Send a special "poison pill" item such as "DONE", or interrupt it.
Interview Corner
How would you solve the producer-consumer problem in Java?
The simplest robust solution uses a BlockingQueue such as ArrayBlockingQueue: producers call put, which blocks when the queue is full, and consumers call take, which blocks when it's empty. A manual solution uses a shared bounded buffer with synchronized methods: put waits in a while loop while the buffer is full, take waits while it's empty, and both call notifyAll after changing the buffer. Shutdown is usually signaled with a poison-pill item or interruption.
Practice
- Easy: Change
QueueVersionto use a queue of capacity 1 and add a short sleep in the consumer. Does the output change? - Harder: Run two producers and two consumers on
BoundedBuffer, each producer sending five items, and make sure all ten items are consumed exactly once.
Key Takeaways
Was this tutorial helpful?