0
0
JavaProgramBeginner · 2 min read

Java Program to Implement Producer Consumer Problem

A Java program to implement the producer consumer problem uses a shared buffer with wait() and notify() methods to synchronize producer and consumer threads, like in class ProducerConsumer { synchronized produce() { wait(); notify(); } synchronized consume() { wait(); notify(); }.
📋

Examples

InputProducer produces 5 items, Consumer consumes 5 items
OutputProduced: 1 Consumed: 1 Produced: 2 Consumed: 2 Produced: 3 Consumed: 3 Produced: 4 Consumed: 4 Produced: 5 Consumed: 5
InputProducer produces 1 item, Consumer consumes 1 item
OutputProduced: 1 Consumed: 1
InputProducer produces 0 items, Consumer consumes 0 items
Output
🧠

How to Think About It

To solve the producer consumer problem, think of a shared box where the producer puts items and the consumer takes items. The producer must wait if the box is full, and the consumer must wait if the box is empty. We use wait() to pause threads and notify() to wake them up when the state changes, ensuring they don't clash or lose data.
📐

Algorithm

1
Create a shared buffer with capacity for one item.
2
Producer checks if buffer is full; if yes, wait.
3
Producer adds an item and notifies consumer.
4
Consumer checks if buffer is empty; if yes, wait.
5
Consumer removes an item and notifies producer.
6
Repeat steps 2-5 for the required number of items.
💻

Code

java
class ProducerConsumer {
    private int item;
    private boolean available = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (available) wait();
        item = value;
        System.out.println("Produced: " + item);
        available = true;
        notify();
    }

    public synchronized void consume() throws InterruptedException {
        while (!available) wait();
        System.out.println("Consumed: " + item);
        available = false;
        notify();
    }
}

public class Main {
    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    pc.produce(i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    pc.consume();
                    Thread.sleep(150);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        producer.start();
        consumer.start();
    }
}
Output
Produced: 1 Consumed: 1 Produced: 2 Consumed: 2 Produced: 3 Consumed: 3 Produced: 4 Consumed: 4 Produced: 5 Consumed: 5
🔍

Dry Run

Let's trace producing and consuming the first two items through the code

1

Producer produces item 1

available = false, so producer sets item = 1, prints 'Produced: 1', sets available = true, calls notify()

2

Consumer consumes item 1

available = true, so consumer prints 'Consumed: 1', sets available = false, calls notify()

3

Producer produces item 2

available = false, so producer sets item = 2, prints 'Produced: 2', sets available = true, calls notify()

4

Consumer consumes item 2

available = true, so consumer prints 'Consumed: 2', sets available = false, calls notify()

StepItemAvailableAction
11false -> trueProduced item 1, notified consumer
21true -> falseConsumed item 1, notified producer
32false -> trueProduced item 2, notified consumer
42true -> falseConsumed item 2, notified producer
💡

Why This Works

Step 1: Shared buffer controls access

The shared buffer uses a boolean available to track if an item is ready, preventing producer and consumer from working at the same time.

Step 2: Producer waits if buffer full

Producer calls wait() when available is true, pausing until consumer consumes the item.

Step 3: Consumer waits if buffer empty

Consumer calls wait() when available is false, pausing until producer produces an item.

Step 4: Notify wakes waiting thread

After producing or consuming, notify() wakes the other thread to continue work, ensuring smooth handoff.

🔄

Alternative Approaches

Using BlockingQueue
java
import java.util.concurrent.ArrayBlockingQueue;

public class Main {
    public static void main(String[] args) {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1);

        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    int val = queue.take();
                    System.out.println("Consumed: " + val);
                    Thread.sleep(150);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        producer.start();
        consumer.start();
    }
}
This approach uses built-in thread-safe queue with blocking methods, simplifying synchronization and reducing errors.
Using Semaphore
java
import java.util.concurrent.Semaphore;

class ProducerConsumer {
    private int item;
    private Semaphore empty = new Semaphore(1);
    private Semaphore full = new Semaphore(0);

    public void produce(int value) throws InterruptedException {
        empty.acquire();
        item = value;
        System.out.println("Produced: " + item);
        full.release();
    }

    public void consume() throws InterruptedException {
        full.acquire();
        System.out.println("Consumed: " + item);
        empty.release();
    }
}

public class Main {
    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    pc.produce(i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    pc.consume();
                    Thread.sleep(150);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        producer.start();
        consumer.start();
    }
}
Using semaphores controls access with permits, providing clear signaling between producer and consumer.

Complexity: O(n) time, O(1) space

Time Complexity

The program runs in O(n) time because the producer and consumer each process n items sequentially.

Space Complexity

Space is O(1) since only one item is stored in the buffer at a time, no extra data structures grow with input.

Which Approach is Fastest?

Using BlockingQueue is often fastest and simplest due to built-in synchronization, while manual wait/notify requires careful handling.

ApproachTimeSpaceBest For
wait/notifyO(n)O(1)Learning synchronization basics
BlockingQueueO(n)O(1)Simple and safe production code
SemaphoreO(n)O(1)Fine control over permits and access
💡
Use wait() and notify() inside synchronized blocks to avoid thread conflicts.
⚠️
Beginners often forget to call wait() inside a loop checking the condition, causing missed signals or deadlocks.