What is Producer Consumer Problem in Java?
Introduction
The producer-consumer problem in Java (also known as the bounded-buffer problem) is a classic multi-process synchronization problem, in which we try to achieve synchronization between more than one process.
In the producer-consumer problem, there are two processes: the producer and the consumer. These processes share a common buffer of fixed size, which is used as a queue. The producer's task is to generate data (item) and put it into the buffer. The consumer's task is to consume data by removing it from the queue.
The problem/complexities in the Producer-Consumer problem:
- The producer should not produce any data when the buffer is full. The producer in this case should wait until the consumer consumes data and some space is cleared in the buffer.
- The consumer can consume data only when the buffer is not empty. If the buffer is empty, the consumer cannot consume data (i.e., cannot remove data from the queue).
- Access to the memory buffer should not be given simultaneously to both the producer and consumer.
- A real-life example of this can be a factory that produces some items and stores them in its storage. The producer will only produce items when there is space left in the storage. If the storage is full, the producer will wait for the consumer to consume items before starting production again. Similarly, the consumer cannot consume an item if the storage is empty. The consumer will wait until there is an item in the storage to consume.
In this article, we have explained three different ways to solve the producer-consumer problem in Java, along with examples.
The Solution to the Producer-Consumer Problem using Semaphore
The Semaphore is a thread synchronization construct that can be used to control access to a shared resource using a variable/counter.
In Java, the Semaphore class is provided in the java.util.concurrent package, which eliminates the need to create it manually. It offers interfaces and methods required for synchronization.
The main methods used in Semaphore are acquire() and release().
acquire() method: This method acquires a permit from the semaphore if one is available and returns immediately. It reduces the number of available permits by 1. If there are no permits available for the current thread, it will go into an inactive state until another thread invokes the release() method or interrupts the current thread. If the current thread is interrupted, an InterruptedException is thrown.
release() method: The release method releases a permit and returns it to the semaphore, increasing the number of available permits by 1. If any thread is waiting to acquire a permit, the semaphore allows it to acquire the released resource.
By utilizing the Semaphore, we can implement a solution to the producer-consumer problem, ensuring proper synchronization between the producer and consumer threads.
Our semaphore solution has four classes:
Producer: To create objects that are producing the item and insert them into the buffer. Consumer: To create objects to consume items from the buffer, Que: This is the queue that we want to synchronize. Main: This is the main class, where we create Producer, Consumer, and Que.
Output:
Explanation:
In the given program, there is a Que class that creates objects for the consumer and producer using the built-in Semaphore class. The Que class also contains the get() and put() methods.
In the get() method, there is a try block because the consumer needs to acquire a permit from the consumer semaphore before consuming an item. This is achieved using the code:
try {
Con.acquire();
}
If the consumer is unable to acquire the permit, it goes into the inactive state until a permit is available or until it is interrupted. The InterruptedException is caught in the catch block. Finally, when the consumer has consumed an item, Prod.release() is called to notify the producer that it can produce another item.
Similarly, in the put() method, a permit is acquired for producing an item using Prod.acquire(). After producing the item, the producer notifies the consumer by calling Con.release().
The program also includes the Producer and Consumer classes, each with their constructor and a loop that runs 5 times. The loop calls the put() method to add items to the buffer queue in the case of the producer, and the get() method to consume items from the buffer queue in the case of the consumer.
In the main class, an object of the Que class is created to initialize the buffer queue. Then, new Consumer(q) and new Producer(q) are called to start the consumer and producer threads, respectively.
Producer-Consumer Solution using Threads in Java
The producer-consumer problem in java can also be solved with the help of threads. A thread is a sequential flow of control in a program. The thread allows a program to operate more efficiently by doing multiple things simultaneously with the help of threads. All Java programs have at least one thread known as the main thread.
The producer has the initial value of 0. We have a synchronized block so that only a producer or a consumer thread runs at a time. The produce function also checks if the list is full, if it is full it gives up the intrinsic lock and on goes on a waiting state.
If the list is empty, then the producer adds an item to the list.
We have the notify() method to notify the consumer to start consuming items from the list.
The consume() function has a loop to get items from the list. It checks if the list is empty, and if it is, the thread gives up the lock and transfers control to the producer thread to produce an item and add it to the list.
If the list is not empty, the consumer removes an item from the list. The notify() method is used to notify the producer to produce an item if needed.
We add a sleep method at the end of both producer and consumer methods so that the output is not shown at once and we can see what is happening in the code.
We can change the value of capacity to change the size of the list. In our case, the list size is 1.
We have the main class in which we create threads t1 and t2 for producer and consumer respectively and then run both the threads.
The full java code will look like this:
Output:
Producer-Consumer Solution using BlockingQueue
The BlockingQueue provides an easier way to implement the producer-consumer problem in Java. It is a part of the java.util.concurrent package.
When the BlockingQueue is full and a producer thread tries to put an item into the queue, it gets blocked until a consumer thread removes an item. Similarly, if the BlockingQueue is empty and a consumer thread tries to get an item, it gets blocked and remains blocked until a producer puts an item into it.
To solve the producer-consumer problem using BlockingQueue, we primarily use two main methods:
put() method: This method adds an item to the BlockingQueue. If the queue is full, it waits until there is space available to insert the item.
take() method: This method removes and returns an item from the BlockingQueue. If the queue is empty, it waits until there is an item available to be removed.
By utilizing the BlockingQueue and its put() and take() methods, we can easily implement a solution to the producer-consumer problem in Java, ensuring proper synchronization and blocking behavior between the producer and consumer threads.
In the main function, we create a BlockingQueue of capacity 5. We then create objects for both the producer and consumer (p1 and c1, respectively) and pass them the common buffer.
After that, we create threads for both the producer and consumer, and start both threads.
The producer class generates numbers in the range [1, 4] and inserts them into the buffer. The consumer class has a function that consumes items by removing them from the buffer. The loop in the consumer stops when the last number (5) is consumed.
Output:
Conclusion
- The producer is responsible for producing an item and adding it to the buffer or list.
- The consumer is responsible for consuming an item by removing it from the list.
- The producer cannot produce an item if the list is already full, and the consumer cannot consume an item if the list is empty.
- The producer-consumer problem can be solved using semaphores, which are thread synchronization constructs used to control access to shared resources.
- Another way to solve the problem is by using threads, which are sequential flows of control in a program.
- The easiest way to solve the producer-consumer problem in Java is by using the BlockingQueue from the java.util.concurrent package, where the put() and take() methods handle blocking if the queue is full or empty, respectively.