Java Program to Implement Producer Consumer Problem
wait() and notify() methods to synchronize producer and consumer threads, like in class ProducerConsumer { synchronized produce() { wait(); notify(); } synchronized consume() { wait(); notify(); }.Examples
How to Think About It
wait() to pause threads and notify() to wake them up when the state changes, ensuring they don't clash or lose data.Algorithm
Code
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(); } }
Dry Run
Let's trace producing and consuming the first two items through the code
Producer produces item 1
available = false, so producer sets item = 1, prints 'Produced: 1', sets available = true, calls notify()
Consumer consumes item 1
available = true, so consumer prints 'Consumed: 1', sets available = false, calls notify()
Producer produces item 2
available = false, so producer sets item = 2, prints 'Produced: 2', sets available = true, calls notify()
Consumer consumes item 2
available = true, so consumer prints 'Consumed: 2', sets available = false, calls notify()
| Step | Item | Available | Action |
|---|---|---|---|
| 1 | 1 | false -> true | Produced item 1, notified consumer |
| 2 | 1 | true -> false | Consumed item 1, notified producer |
| 3 | 2 | false -> true | Produced item 2, notified consumer |
| 4 | 2 | true -> false | Consumed 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
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(); } }
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(); } }
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.
| Approach | Time | Space | Best For |
|---|---|---|---|
| wait/notify | O(n) | O(1) | Learning synchronization basics |
| BlockingQueue | O(n) | O(1) | Simple and safe production code |
| Semaphore | O(n) | O(1) | Fine control over permits and access |
wait() and notify() inside synchronized blocks to avoid thread conflicts.wait() inside a loop checking the condition, causing missed signals or deadlocks.