Producer Consumer design pattern

Definition

The producer consumer pattern is a concurrency design pattern where one or more producer threads produce objects which are queued up, and then consumed by one or more consumer threads. The objects enqueued often represent some work that needs to be done. Decoupling the detection of work from the execution of work means you can control how many threads at a time that are engaged in detecting or executing the work.

Real World Example

It is everywhere in real life and depicts coordination and collaboration.

example:

One person is preparing food (Producer) while other one is serving food (Consumer). Both will use shared table for putting food plates and taking food plates. Producer - which is the person preparing food will wait if table is full and Consumer (Person who is serving food) will wait if table is empty. Table is a shared object here. On Java library Executor framework itself implement Producer Consumer design pattern be separating responsibility of addition and execution of task.

Benefits

Used most commonly while writing multi-threaded or concurrent code.

  1. Producer Consumer Pattern simplifies development. You can write Producer code and Consumer code independently and Concurrently, they just need to know shared object.
  2. Producer doesn’t need to know about who the consumer is or how many consumers there are. Same is true with Consumer.
  3. Producer and Consumer can work with different speeds. There is no risk of Consumer consuming half-baked items. In fact, by monitoring consumer speed, one can introduce more consumers for better utilization.
  4. Separating producer and consumer functionality results in more clean, readable and manageable code.

Use Cases

There are several different kinds of use cases for the producer consumer design pattern. Some of the most common are:

Reduce foreground thread latency.

In some systems, you have a single foreground thread which has the communication with the outside world. In a server it could be the thread accepting the incoming connections from clients. In a desktop app that could be the UI thread.

In order to not make the foreground thread busy with tasks - whatever tasks the foreground thread receives from the outside world are offloaded to one or more background threads. In a server it could be processing the data that is received via the inbound connections.

In a desktop app the foreground thread (UI thread) could be responding to the user events - a.g. opening a file, or downloading a file, or saving a file etc. To avoid having the UI thread blocked so the whole GUI is unresponsive, the UI thread can offload long-running tasks to background worker threads.

Load balance work between different threads.

Another type of use case for the producer consumer pattern is to load balance work between a set of worker threads. Actually, this load balancing happens pretty much automatically, as long as the worker threads take new task objects from the queue as soon as they have time to process them. This will result in load balancing the tasks between the worker threads.

Backpressure management.

If the queue between the producer and consumer threads is a Java BlockingQueue, then you can use the queue for backpressure management. This is another built-in feature of the producer consumer pattern.

Backpressure means, that if the producer thread(s) produce more work than the consumer threads are able to handle - the tasks will queue up in the queue. At some point the BlockingQueue will become full, and the producer threads will be blocked trying to insert new tasks / work objects into the queue. This phenomenon is called backpressure. The system presses back against the producers - towards the beginning of the “pipeline” - preventing more work from coming in.

The backpressure will “spill out” of the queue, and slow down the producer thread(s). Thus, they too could propagate the pressure back up the work pipeline, if there are any earlier steps in the total pipeline.

Producer Consumer Problem in Multi-threading

Producer should wait if Queue or bucket is full and Consumer should wait if queue or bucket is empty. This problem can be implemented or solved by different ways in Java. Classical way is using wait() and notify() methods to communicate between Producer and Consumer thread and blocking each of them on individual conditions like full queue and empty queue. With introduction of BlockingQueue Data Structure in Java 5, it is now much simpler because BlockingQueue provides this control implicitly by introducing blocking methods put() and take(). Now you don’t require to use wait() and notify() to communicate between Producer and Consumer. BlockingQueue put() method will block if Queue is full in case of Bounded Queue and take() will block if Queue is empty.

Using Blocking Queue to implement Producer Consumer Pattern

BlockingQueue amazingly simplifies implementation of Producer-Consumer design pattern by providing out-of-box support of blocking on put() and take(). Developers don’t need to write confusing and critical piece of wait()-notify() code to implement communication. BlockingQuue is an interface and Java 5 provides different implementation like ArrayBlockingQueue and LinkedBlockingQueue, both implement FIFO order or elements, while ArrayLinkedQueue is bounded in nature LinkedBlockingQueue is optionally bounded.

Here is a complete code example of Producer Consumer pattern with BlockingQueue. Compare it with classic wait() notify() code, it is much simpler and easy to understand.

Producer Class in java:

class Producer implements Runnable {

    private final BlockingQueue sharedQueue;

    public Producer(BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        for(int i=0; i<10; i++) {
            try {
                System.out.println("Produced: " + i);
                sharedQueue.put(i);
            } catch (InterruptedException ex) {
                Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

Consumer Class in Java:

class Consumer implements Runnable {
    private final BlockingQueue sharedQueue;

    public Consumer (BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        while(true){
            try {
                System.out.println("Consumed: "+ sharedQueue.take());
            } catch (InterruptedException ex) {
                Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

Main class:

import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ProducerConsumerPattern {
    public static void main(String args[]){

        //Creating shared object BlockingQueue sharedQueue = new LinkedBlockingQueue();

        //Creating Producer and Consumer Threads
        Thread prodThread = new Thread(new Producer(sharedQueue));
        Thread consThread = new Thread(new Consumer(sharedQueue));

        //Starting producer and Consumer threads
        prodThread.start();
        consThread.start();
    }
}

Output:

Produced: 0
Produced: 1
Consumed: 0
Produced: 2
Consumed: 1
Produced: 3
Consumed: 2
Produced: 4
Consumed: 3
Produced: 5
Consumed: 4
Produced: 6
Consumed: 5
Produced: 7
Consumed: 6
Produced: 8
Consumed: 7
Produced: 9
Consumed: 8
Consumed: 9

Reading material

https://jenkov.com/tutorials/java-concurrency/producer-consumer.html

KNOWLEDGE GAP - IMPLEMENT THIS

How do do this code using put() and take().