Sometimes in our applications, we require two or more threads to communicate with each other. One such most common use case is where we have a queue from which one thread is consuming data and the other data is producing data and putting in the queue.
Here we have to check if the queue has some data before consuming it. The straightforward method to achieve this polling. In polling, we check if the queue is filled with some data in a loop. If the queue is not empty, we take some action to consume data. This method is inefficient as it wastes CPU cycles.
In this example three threads will wait if boolean variable go is false, remember boolean go is a volatile variable so that all threads will see its updated value. Initially, three threads W1, W2, W3 will wait because variable go is false than one thread N1 will make go true and notify all threads by calling notifyAll() method or notify() just one thread by calling notify() method.
In the case of notify() call, there is no guarantee which thread will wake up and we can see it by running this program multiple times.
In the case of notifyAll(), all thread will wake up but they will compete for monitor or lock and the Thread which will get the lock first will finish its execution and resetting go to false which will force other two threads still waiting.
At the end of this program, we will have two threads waiting and two threads including notification thread finished. The program will not terminate because the other two threads are still waiting and they are not daemon threads.
Here we have to check if the queue has some data before consuming it. The straightforward method to achieve this polling. In polling, we check if the queue is filled with some data in a loop. If the queue is not empty, we take some action to consume data. This method is inefficient as it wastes CPU cycles.
Inter-Thread Communication
To avoid polling, Java has a mechanism called inter-thread communication in which one thread sends a notification to the other thread(s) once a certain condition is met. This mechanism has three methods, all of which are in the java.lang.Object class -
- wait() - This method pauses the thread and lets it do nothing while a certain condition is true. For e.g., in our producer-consumer problem, the consumer thread has to wait until there is some element in the queue. On the other hand, the producer thread has to wait till the time the queue is full.
- notify() - This method wakes up only one thread waiting on the object and that threads start running. If there is more than one thread waiting on the object, this method wakes up only one of them. But which thread will be woken up depends on the OS implementation.
- notifyAll() - This method wakes up all the threads waiting on the object. But which one will be processed first, depends entirely on the OS implementation.
Producer Consumer problem
This problem demonstrates an excellent use case of wait() and notify()/notifyAll() methods. This problem has the following constraints
- You are given a queue of a specified size in which value can be added as well values can be removed from it.
- The thread which adds the value is called producer. A producer can only add a value if the queue is not full, i.e. the size of the queue < max size.
- The thread which consumes the value is called consumer. A consumer can only consume from the queue if the queue is not empty, i.e. the size of the queue != 0.
- If the queue is full, then the producer needs to wait and communicate the consumer to consume from the queue.
- If the queue is empty, then the consumer has to wait and communicate the producer to add some values in the queue.
- This communication is achieved via wait() and notify()/notifyAll() methods. Below code demonstrates how -
Here the producer waits when the queue is full and notifies the consumer to consumer value from the queue after adding some value to it. On the other hand, the consumer waits when the queue is empty and notifies the producer after consuming a value. The above code will have the following output -
Queue is empty!
Produced value: 84
Produced value: 69
Produced value: 5
Produced value: 15
Queue is full!
Consumer value: 84
Consumer value: 69
Consumer value: 5
Consumer value: 15
Queue is empty!
Produced value: 84
Produced value: 69
Produced value: 5
Produced value: 15
Queue is full!
Consumer value: 84
Consumer value: 69
Consumer value: 5
Consumer value: 15
Calling wait() & notify()/notify() from if or while block
If we call wait() inside the if block, it may lead to some errors because it's possible for a thread to wake up spuriously even when waiting condition is not changed.
If we don't check the condition again after waking up by using a loop, we might take the wrong action which may cause problem e.g. trying to insert item on a full queue or trying to consume from an empty queue. That's why we should always call wait and notify method from a loop and not from if block.
Why should we call wait(), notify(), notifyAll() from synchronized block/method?
In our producer-consumer problem, when we call notify()/notifyAll(), a notification is issued to a single/multiple threads that the waiting condition is changed. Once the notification leaves the synchronized block, all the threads fight for the lock on which they are waiting. One lucky thread returns from the wait() and proceeds further.
Let's divide the whole scenario into steps
- Producer tests the condition (is queue full?) and decides it must wait (if the queue is full).
- The consumer sets condition after consuming an element from the queue.
- Consumer calls notify() but this goes unheard as the Producer is not yet waiting.
- Producer thread calls the wait() and now goes in the waiting state.
We can clearly see a race condition here which causes us to lose a notification and this will give us the wrong result.
Thus, it is always a good practice to acquire the lock of the object before calling these methods. Since the wait() method also releases the lock prior to waiting and reacquires the lock prior to returning from the wait() method, we must use this lock to ensure that checking the condition (queue is full or not) and setting the condition (taking element from queue) is atomic which can be achieved by using synchronized method or block.
Difference between notify() and notifyAll()
When we call notify() only one of the waiting thread wakes up but it is not guaranteed which thread will be woken up. While if we call notifyAll(), all threads waiting on the lock will be woken up and then fight to acquire the lock.
This is the reason wait() is called on the loop so that if multiple threads are woken up, the thread which will get the lock first execute and it may reset the waiting condition, which will force the subsequent threads to wait.
Following example shows that only one thread is woken up when we call notify() and all threads are woken up when we call notifyAll() -
In this example three threads will wait if boolean variable go is false, remember boolean go is a volatile variable so that all threads will see its updated value. Initially, three threads W1, W2, W3 will wait because variable go is false than one thread N1 will make go true and notify all threads by calling notifyAll() method or notify() just one thread by calling notify() method.
In the case of notify() call, there is no guarantee which thread will wake up and we can see it by running this program multiple times.
In the case of notifyAll(), all thread will wake up but they will compete for monitor or lock and the Thread which will get the lock first will finish its execution and resetting go to false which will force other two threads still waiting.
At the end of this program, we will have two threads waiting and two threads including notification thread finished. The program will not terminate because the other two threads are still waiting and they are not daemon threads.
Output in case of notify() call
Thread[W1,5,main] is going to wait on this object
Thread[W2,5,main] is going to wait on this object
Thread[W3,5,main] is going to wait on this object
Thread[N1,5,main] is going to notify all the threads waiting on this object
Thread[W1,5,main] is woken up
Thread[W1,5,main] finished execution
Thread[N1,5,main] finished execution
Output in case of notifyAll() call
Thread[W1,5,main] is going to wait on this object
Thread[W3,5,main] is going to wait on this object
Thread[W2,5,main] is going to wait on this object
Thread[N1,5,main] is going to notify all the threads waiting on this object
Thread[N1,5,main] finished execution
Thread[W1,5,main] is woken up
Thread[W1,5,main] finished execution
Thread[W2,5,main] is woken up
Thread[W2,5,main] is going to wait on this object
Thread[W3,5,main] is woken up
Thread[W3,5,main] is going to wait on this object
Comments
Post a Comment