Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
The producer-consumer pattern is classic in concurrent, multithreaded programming. It simplifies the development pattern by decoupling producers and consumers through separate execution work, allowing producers and consumers to produce and consume data at different rates. In this article we will take a look at what the producer-consumer model is, which is often mentioned in multi-threaded interview questions. How to solve the producer consumer pattern using a Blocking Queue and the benefits of using the producer consumer pattern.
Producer-consumer models in the real world
The pattern of producer and consumer can be seen everywhere in life. It describes the relationship between coordination and collaboration. For example, one person is preparing food (the producer) and another person is eating (the consumer), they use a shared table for placing and taking away dishes, the producer prepares the food and waits if the table is full, and the consumer (the eater) waits if the table is empty. Here the table is a shared object. The Java Executor framework itself implements the producer-consumer pattern, which is responsible for adding and performing tasks, respectively.
Benefits of the producer-consumer model
It is indeed a useful design pattern for writing multithreaded or concurrent code. Here are some of its advantages:
It simplifies development, you can write consumers and producers independently or concurrently, it just needs to know who the shared object is
The producer does not need to know who the consumer is or how many consumers there are, and the same goes for the consumer
Producers and consumers can execute at different speeds
Separated consumers and producers write simpler, readable, and maintainable code functionally
Producer-consumer problems in multithreading
Producer consumer question is a popular interview question where the interviewer asks you to implement the producer consumer design pattern so that the producer should wait if the queue or basket is full and the consumer should wait if the queue or basket is empty. This problem can be realized in different ways. The classic approach is to use wait and notify to cooperate between producer and consumer threads, blocking if the queue is full or empty. Java5’s BlockingQueue data structure is simpler because it implicitly provides these controls. Now that you don’t need to use wait and nofity to communicate between producers and consumers, the put() method that blocks the queue will block if the queue is full and the queue take() method will block if the queue is empty. You can see code examples in the next section.
Implement the producer-consumer pattern using blocking queues
Blocking queues are super simple to implement the producer-consumer pattern, providing out-of-the-box support for blocking methods put() and take() without requiring developers to write confusing wait-nofity code to communicate. BlockingQueue Is an interface that Java5 provides for different realities, such as ArrayBlockingQueue and LinkedBlockingQueue, both of which are first-in, first-out (FIFO) orders. Whereas ArrayLinkedQueue is naturally bounded, the optional boundary of the LinkedBlockingQueue. Here is a complete example of producer consumer code that is easier to understand than traditional Wait and Nofity code. We are often asked to write a producer consumer model based on blocking queues:
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[]) throws Exception {
BlockingQueue sharedQueue = new LinkedBlockingQueue();
Thread prodThread = new Thread(new Producer(sharedQueue));
Thread consThread = new Thread(newConsumer(sharedQueue)); prodThread.start(); consThread.start(); }}class Producer implements Runnable {
private final BlockingQueue sharedQueue;
public Producer(BlockingQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}
@Override
public void run(a) {
for (int i = 0; i < 10; i++) {
try {
System.out.println("Produced by the producer:" + i);
sharedQueue.put(i);
} catch (InterruptedException ex) {
Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex); }}}}class Consumer implements Runnable {
private final BlockingQueue sharedQueue;
public Consumer(BlockingQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}
@Override
public void run(a) {
while (true) {
try {
System.out.println("Consumer Consumed:" + sharedQueue.take());
} catch (InterruptedException ex) {
Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex); }}}}Copy the code