๐ฝ๏ธ Producer-Consumer Problem (Multithreading)

๐ง Problem Statement
Section titled โ๐ง Problem StatementโA classic synchronization problem where:
- ๐จโ๐ญ A Producer creates data and adds it to a shared buffer
- ๐งโ๐ง A Consumer takes data from the buffer to process
- ๐ฆ The buffer has a fixed capacity, requiring proper coordination
โ Constraints
Section titled โโ Constraintsโ- Buffer Capacity: Buffer has a fixed maximum size that cannot be exceeded
- Thread Safety: Must handle concurrent access from producer and consumer threads
- No Busy Waiting: Threads should not consume CPU while waiting
- Proper Coordination: Producer must wait when buffer is full, consumer must wait when buffer is empty
- Data Integrity: No data should be lost or duplicated
- Fair Access: Both producer and consumer should get reasonable access to the buffer
๐ฅ Input / Output
Section titled โ๐ฅ Input / OutputโInput:
- Producer threads generating data items
- Consumer threads requesting data items
- Shared buffer with limited capacity
Output:
- Controlled data flow through the buffer
- Threads properly synchronized without race conditions
- Efficient resource utilization
๐ก Intuition About Solving This Problem
Section titled โ๐ก Intuition About Solving This ProblemโThe key insight is that we need a shared buffer that both producer and consumer can access safely. The producer should wait when the buffer is full, and the consumer should wait when the buffer is empty. We use synchronization primitives like wait() and notify() or Condition variables to coordinate between threads efficiently.
๐ Steps to Take to Solve
Section titled โ๐ Steps to Take to Solveโ- Design the shared buffer: Create a thread-safe data structure with capacity limits
- Implement producer logic: Add items when space is available, wait when full
- Implement consumer logic: Remove items when available, wait when empty
- Add synchronization: Use locks and condition variables for thread coordination
- Handle waiting: Implement proper wait/notify mechanisms
- Manage thread lifecycle: Handle thread interruption and shutdown
โ ๏ธ Edge Cases to Consider
Section titled โโ ๏ธ Edge Cases to Considerโ- Buffer overflow: Handle when producer tries to add to full buffer
- Buffer underflow: Handle when consumer tries to remove from empty buffer
- Thread interruption: Handle InterruptedException properly
- Spurious wakeups: Use while loops to check conditions after waking up
- Multiple producers/consumers: Handle scenarios with more than one of each
- Shutdown coordination: Properly terminate all threads
๐ป Code (with comments)
Section titled โ๐ป Code (with comments)โSolution 1: Using wait() and notify()
Section titled โSolution 1: Using wait() and notify()โimport java.util.LinkedList;import java.util.Queue;
class SharedBuffer { private Queue<Integer> queue = new LinkedList<>(); private int capacity;
public SharedBuffer(int capacity) { this.capacity = capacity; }
public synchronized void produce(int item) throws InterruptedException { // Wait until there's space in the buffer while (queue.size() == capacity) { System.out.println("Buffer is full! Producer is waiting..."); wait(); } queue.add(item); System.out.println("Produced: " + item); notify(); // Notify waiting consumer }
public synchronized int consume() throws InterruptedException { // Wait until there's data in the buffer while (queue.isEmpty()) { System.out.println("Buffer is empty! Consumer is waiting..."); wait(); } int item = queue.poll(); System.out.println("Consumed: " + item); notify(); // Notify waiting producer return item; }}Producer & Consumer Threads
Section titled โProducer & Consumer Threadsโclass Producer extends Thread { private SharedBuffer buffer;
public Producer(SharedBuffer buffer) { this.buffer = buffer; }
public void run() { int item = 1; while (true) { try { buffer.produce(item++); Thread.sleep(1000); // Simulate production time } catch (InterruptedException e) { e.printStackTrace(); } } }}
class Consumer extends Thread { private SharedBuffer buffer;
public Consumer(SharedBuffer buffer) { this.buffer = buffer; }
public void run() { while (true) { try { buffer.consume(); Thread.sleep(2000); // Simulate consumption time } catch (InterruptedException e) { e.printStackTrace(); } } }}Main Method
Section titled โMain Methodโpublic class ProducerConsumerExample { public static void main(String[] args) { SharedBuffer buffer = new SharedBuffer(5); new Producer(buffer).start(); new Consumer(buffer).start(); }}Solution 2: Using ReentrantLock and Condition
Section titled โSolution 2: Using ReentrantLock and Conditionโimport java.util.LinkedList;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
class ProducerUsingLock implements Runnable { private final LinkedList<Integer> queue; private final int capacity; private final Lock lock; private final Condition notFull; private final Condition notEmpty;
public ProducerUsingLock(LinkedList<Integer> queue, int capacity, Lock lock, Condition notFull, Condition notEmpty) { this.queue = queue; this.capacity = capacity; this.lock = lock; this.notFull = notFull; this.notEmpty = notEmpty; }
@Override public void run() { int item = 0; try { while (true) { lock.lock(); try { // Wait until there's space in the buffer while (queue.size() == capacity) { notFull.await(); }
queue.add(item); System.out.println("Produced: " + item++); notEmpty.signal(); // Signal waiting consumer } finally { lock.unlock(); } Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } }}class ConsumerUsingLock implements Runnable { private final LinkedList<Integer> queue; private final Lock lock; private final Condition notFull; private final Condition notEmpty;
public ConsumerUsingLock(LinkedList<Integer> queue, Lock lock, Condition notFull, Condition notEmpty) { this.queue = queue; this.lock = lock; this.notFull = notFull; this.notEmpty = notEmpty; }
@Override public void run() { try { while (true) { lock.lock(); try { // Wait until there's data in the buffer while (queue.isEmpty()) { notEmpty.await(); }
int item = queue.removeFirst(); System.out.println("Consumed: " + item); notFull.signal(); // Signal waiting producer } finally { lock.unlock(); } Thread.sleep(2000); } } catch (InterruptedException e) { e.printStackTrace(); } }}Main Method for Lock-based Solution
Section titled โMain Method for Lock-based Solutionโpublic class ReentrantLockProducerConsumer { public static void main(String[] args) { LinkedList<Integer> queue = new LinkedList<>(); int capacity = 5; Lock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition();
new Thread(new ProducerUsingLock(queue, capacity, lock, notFull, notEmpty)).start(); new Thread(new ConsumerUsingLock(queue, lock, notFull, notEmpty)).start(); }}๐ Dry Run Example
Section titled โ๐ Dry Run ExampleโScenario: Buffer capacity 3, 1 producer, 1 consumer
Initial State:
- Buffer: [] (empty)
- Producer: ready to produce
- Consumer: waiting for data
Step 1: Producer produces item 1
- Check: buffer.size() < capacity โ true (0 < 3)
- Add item 1 to buffer
- Buffer: [1]
- Notify consumer
- Consumer wakes up and consumes item 1
- Buffer: []
Step 2: Producer produces item 2
- Check: buffer.size() < capacity โ true (0 < 3)
- Add item 2 to buffer
- Buffer: [2]
- Notify consumer
- Consumer consumes item 2
- Buffer: []
Step 3: Producer produces items 3, 4, 5
- Buffer: [3, 4, 5] (at capacity)
- Producer tries to produce item 6
- Check: buffer.size() < capacity โ false (3 >= 3)
- Producer waits
Step 4: Consumer consumes item 3
- Buffer: [4, 5]
- Notify producer
- Producer wakes up and produces item 6
- Buffer: [4, 5, 6]
Result: Producer and consumer coordinate properly, buffer never exceeds capacity.
โฑ๏ธ Time Complexity / Space Complexity
Section titled โโฑ๏ธ Time Complexity / Space ComplexityโTime Complexity:
- Production: O(1) - constant time to add item to buffer
- Consumption: O(1) - constant time to remove item from buffer
- Waiting: O(1) - constant time to block/unblock threads
Space Complexity:
- Buffer storage: O(n) where n is the buffer capacity
- Thread overhead: O(1) - constant space for producer and consumer threads
- Lock overhead: O(1) - constant space for synchronization objects
๐ง Challenges
Section titled โ๐ง Challengesโ- ๐งต Race conditions between threads
- ๐ Avoiding busy waiting (inefficient CPU usage)
- โ Ensuring thread-safe access to the buffer
๐ก
wait()andnotify()must be used inside synchronized blocks!
๐ง Key Concepts
Section titled โ๐ง Key Conceptsโ- ๐ Producer waits when buffer full
- โ Consumer waits when buffer empty
- ๐
notify()wakes a waiting thread
๐ notify() vs notifyAll()
Section titled โ๐ notify() vs notifyAll()โnotify(): Wakes one waiting thread (could be producer or consumer)notifyAll(): Wakes all threads- ๐ Use
notifyAll()for complex conditions or multiple consumers/producers
๐ Summary
Section titled โ๐ Summaryโ| Feature | wait/notify | ReentrantLock & Condition |
|---|---|---|
| Simplicity | โ Easy to learn | โ Slightly complex |
| Flexibility | โ Less control | โ Fine-grained control |
| Performance | โ Efficient | โ Efficient |
| Best Use Case | Simple scenarios | Complex coordination |
๐งช Sample Output
Section titled โ๐งช Sample OutputโProduced: 1Produced: 2Consumed: 1Produced: 3Consumed: 2...๐ Interview Tip
Section titled โ๐ Interview TipโBe ready to explain:
- Why buffer coordination matters
- Difference between intrinsic locks (
synchronized) vsReentrantLock - When to use
notifyAll()instead ofnotify()