Producer-Consumer Problem in Java

Naveen AnamaneniUpdated · 5 min read

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

SituationWhat must happen
Buffer is fullProducers wait until a consumer removes an item
Buffer is emptyConsumers wait until a producer adds an item
An item is added or removedWaiting threads are notified
Producers wait when the buffer is full; consumers wait when it is empty

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.

BoundedBuffer.java▶ Try it
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));
    }
}
Output
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: true

Explanation: 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:

QueueVersion.java▶ Try it
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();
    }
}
Output
Printed ticket for Navika
Printed ticket for Srihitha
Printed ticket for Advitha

Explanation: 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

  1. Easy: Change QueueVersion to use a queue of capacity 1 and add a short sleep in the consumer. Does the output change?
  2. Harder: Run two producers and two consumers on BoundedBuffer, each producer sending five items, and make sure all ten items are consumed exactly once.

Quick check: Producer-consumer

4 questions
1When must a producer wait?
2Why use notifyAll instead of notify here?
3Which class solves producer-consumer for you?
4What is a poison pill?

Key Takeaways

Was this tutorial helpful?

Lesson 406 of 482