Solving the Producer Consumer Problem in java using wait-notify
The Producer Consumer problem is one of the basic questions asked in an interview testing on Multi-threading concepts. A producer is a process that generates data and adds it to a fixed size shared object which may be a list, a queue, a stack -whatever you may choose. A consumer is a process that will simultaneously consume the data from that shared object.
Things get complicated when a consumer starts consuming at a speed greater than that at which the data is being produced. It will then have to deal with an empty shared object. In such type of cases , a consumer has to wait , the producer will notify the consumer when it has added data and the consumer can pick up from then on. There may also be a case when the producer produces at a speed greater than that the consumer can consume , so the shared object is full of data. In that case, the producer has to wait. When the consumer has consumed some data to create space in the shared object, it can notify the producer, which can then start its work again.
Here is my small and simple solution to this problem. The shared object I have chosen is a List of Integers, and I don't allow the list to have more than 10 objects. There are two threads - one Producer thread producing data which is actually any Random number between 0 to 10 and another Consumer thread consuming data by removing the last added data from the list .
If the size of the list is equal to 10, then the Producer thread should not add data to the list. It should then wait for the Consumer to remove some data and notify it. Once it gets the notification, it can again start adding data to the list.
If the list is empty, the Consumer needs to wait for the data to be added to the list. Once the Producer adds some data to the list it will notify the waiting Consumer thread of the same, and it can again start consuming the data.
An important point to note is that we want the two threads to behave nicely and not try to step on each other's shoes. Putting it in the Comp. Sc. terminology, we want to avoid the race condition. So, only one thread should add to/remove from the shared object at a time. We need the two threads to work in tandem here, and so we need the add/remove in their respective synchronized methods/blocks so that only one of them can access the shared object at a time. To further co-ordinate the threads, based on what condition which one should wait and which one should complete its action and notify others, we use the wait and notify methods. A thread should hold the monitor/lock on the object it is calling wait / notify on. Otherwise, an IllegalMonitorStateException is thrown. Once the wait() method is called, the thread relinquishes the lock it holds on the object. This lock may be acquired by any of the waiting threads, and once it is done with its action, it will call notify(). This will wake up the waiting thread, but it will then again have to fight with the other contenders of the lock to gain access to the lock.
So, finally coming to the code , here it is. It is not very elegant, rather it is just one monolithic program. It can be refactored to separate the concerns - like an object whose job is to produce data and consume data and another which handles the threading and synchronization part, but that is left for the reader to do. :)
Things get complicated when a consumer starts consuming at a speed greater than that at which the data is being produced. It will then have to deal with an empty shared object. In such type of cases , a consumer has to wait , the producer will notify the consumer when it has added data and the consumer can pick up from then on. There may also be a case when the producer produces at a speed greater than that the consumer can consume , so the shared object is full of data. In that case, the producer has to wait. When the consumer has consumed some data to create space in the shared object, it can notify the producer, which can then start its work again.
Here is my small and simple solution to this problem. The shared object I have chosen is a List of Integers, and I don't allow the list to have more than 10 objects. There are two threads - one Producer thread producing data which is actually any Random number between 0 to 10 and another Consumer thread consuming data by removing the last added data from the list .
If the size of the list is equal to 10, then the Producer thread should not add data to the list. It should then wait for the Consumer to remove some data and notify it. Once it gets the notification, it can again start adding data to the list.
If the list is empty, the Consumer needs to wait for the data to be added to the list. Once the Producer adds some data to the list it will notify the waiting Consumer thread of the same, and it can again start consuming the data.
An important point to note is that we want the two threads to behave nicely and not try to step on each other's shoes. Putting it in the Comp. Sc. terminology, we want to avoid the race condition. So, only one thread should add to/remove from the shared object at a time. We need the two threads to work in tandem here, and so we need the add/remove in their respective synchronized methods/blocks so that only one of them can access the shared object at a time. To further co-ordinate the threads, based on what condition which one should wait and which one should complete its action and notify others, we use the wait and notify methods. A thread should hold the monitor/lock on the object it is calling wait / notify on. Otherwise, an IllegalMonitorStateException is thrown. Once the wait() method is called, the thread relinquishes the lock it holds on the object. This lock may be acquired by any of the waiting threads, and once it is done with its action, it will call notify(). This will wake up the waiting thread, but it will then again have to fight with the other contenders of the lock to gain access to the lock.
So, finally coming to the code , here it is. It is not very elegant, rather it is just one monolithic program. It can be refactored to separate the concerns - like an object whose job is to produce data and consume data and another which handles the threading and synchronization part, but that is left for the reader to do. :)
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MultiThreadingProblem {
public static void main(String[] args) throws InterruptedException {
List<Integer> sharedList = new ArrayList<>();
//The Producer thread
Thread prodThread = new Thread(() -> {
//synchronized on the shared list
synchronized(sharedList) {
while(true) {
while(sharedList.size() >= 10) {
try {
//wait until we get an empty space in the list
sharedList.wait(); // wait needs to be called on the object we are locking on
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sharedList.add(new Random().nextInt(10));
System.out.println("Producer:" + sharedList);
//notify the Consumer that the list is not empty now, I have added something to it
sharedList.notify(); // notify needs to be called on the object we are locking on
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
//The consumer thread
Thread consThread = new Thread(() -> {
while(true) {
synchronized(sharedList) {
while(sharedList.size() == 0) {
try {
//wait until something is added to the list
sharedList.wait();// wait needs to be called on the object we are locking on
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Consumer:" + sharedList.remove(sharedList.size() - 1));
//notify producer that there is some space in the list
sharedList.notify(); // notify needs to be called on the object we are locking on
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
prodThread.start();
consThread.start();
prodThread.join();
consThread.join();
}
}
Comments