Java Wait Example
1. Introduction
In this article, we will work on an example to implement wait, notify, notifyAll in Java multithreaded environment.
Thread is a lightweight process within java process. Multithreading helps in maximizing CPU utilization. It allows concurrent execution of multiple parts of java program using threads. All java programs (multithreaded or not) by default, start within a thread named as main thread.
In this example, we are going to demonstrate how to enable communication between multiple threads that are using same resource/object.
2. Java Thread Basic Methods
2.1 join
public final void join() throws InterruptedException
Thread class provides join method to allow one thread to wait for another thread until another one completes execution. Calling this function puts the current thread on wait until the thread it is called upon, finishes execution.
This method throws InterruptedException if the thread is interrupted.
public final void join(long millis) throws InterruptedException
There is another variant of join method, which takes milliseconds as argument where current thread waits only for the given milliseconds for other thread to complete.
2.2 sleep
sleep method takes milliseconds as an argument and it puts current thread to sleep for the milliseconds passed to the method relinquishing any lock it is holding right now. If thread is interrupted while it has been put to sleep then the method throws InterruptedException exception.
public static void sleep(long millis) throws InterruptedException
2.3 yield
public static void yield()
yield method indicates that current thread can yield the control of the CPU and any other thread can take control of CPU, if required. Scheduler can ignore the indication or if there is no other thread to give control, then the current thread will continue running.
2.4 start
public void start()
Invoking start method on a thread object spawns a new thread in java process and start() method internally calls run() to execute the new created thread. If start method is invoked again then an IllegalStateException exception will be thrown.
2.5 run
public void run()
As mentioned above, start method invokes run method internally when a new thread is spawned. If we invoke run directly (without start() invocation) then it will not spawn new thread but run will execute in the stack of the current thread only like a regular method call.
3. Synchronization in Java
In a multithreaded environment, threads work on and manipulate shared resources. Synchronization in java provides mutual exclusive access of shared resource to threads. This helps in preserving the right state of the resource as well as prevents dirty read of the resource.
synchronized
keyword provides lock of the object that ensures the mutually exclusive access and prevents race condition.
3.1 synchronized method
synchronized
keyword when used with method indicates that the thread will get lock on the class object.
public synchronized void setValue(int value){ this.value = value; }
synchronized
keyword when used with static method indicates, that the thread will get lock on class rather than object
public static synchronized int getValue(){ return value; }
3.2 synchronized block
synchronized
block is same as synchronized
method but sometimes we don’t want to block the whole method but only a section which manipulates shared resource.
synchronized block takes object as argument. Thread obtains lock on the object when it starts executing synchronized block and relinquishes it at exit of the synchronized block. Like, in double checked locking in case of Singleton object creation
public static SingletonClass getInstance(){ if(instance == null){ synchronized(SingletonClass.class){ if(instance == null) instance = new SingletonClass() } } return instance; }
There are other constructs available to acquire lock like using volatile
, atomic variables or using Lock
interface explicitly.
4. wait(), notify() and notifyAll()
Java wait(), notify() and notifyAll() all are defined in Object class which means any type of object in Java can invoke these method to wait or notify for a shared resource
4.1 wait
public final void wait() throws InterruptedException
When invoked, it causes current thread to go in waiting state for the object until another thread notifies it.
There are two other variants of wait method, which takes waiting time in milliseconds as argument. Thread waits for notification only till waiting time expires and then comes back in runnable state.
If thread is interrupted while waiting then the method throws InterruptedException exception.
4.2 notify
public final void notify()
notify method when invoked sends notification to one of the waiting threads to acquire lock of the shared resource. If multiple threads are waiting on same resource then, notify will send notification to one of them in no particular order.
4.3 notifyAll
public final void notifyAll()
notifyAll method is same as notify but notifyAll notifies all of the waiting threads unlike notify. Though only of the awakened thread will be able to acquire lock of resource, while other threads will go in wait again most probably or exit.
4.4 Thread States
Below diagram shows the lifecycle of the thread from its creation till the exit.
5. Example
We will now see a working example of how to use wait and notify to setup communication between multiple threads.
We will be working on a Producer-Consumer example here. Basically, Producer will produce an element and push it in a shared resource, an Arraylist in our case, while Consumer will consume an element from the shared resource.
Producer and Consumer will use wait and notify to inform other when it can continue.
5.1 Producer
Let’s first define our Producer. Here are few things to keep in mind while defining Producer:
- It must implements Runnable, so new producer thread can be created. There is another option of extending Thread class, but we have not used that as Java classes can only extend from one class but can implement many interfaces, so this approach provides flexibility.
- Producer need to have access to the shared resource i.e.
sharedList
in this case, which is defined as instance variable in Producer class. - We have defined a limit on list,
maxCount
, that how many elements it can hold before producer has to stop and wait for consumer to consume few elements from the list. - A constructor to initialize both
sharedList
andmaxCount
while creating thread.
Constructor of Producer to initialize shared resource and limit
class Producer implements Runnable { List sharedList; int maxCount = 0; int elementCount = 0; public Producer(List sharedList, int maxCount) { this.sharedList = sharedList; this.maxCount = maxCount; } ... }
- Since Producer class implements Runnable interface, we need to provide an overridden definition of run method.
- As mentioned above, run method contains the code that thread executes once it starts
- run is invoked internally by start method and we won’t invoke run directly from code.
- In below code, run invokes another local method produce which produces an element and adds it in shared resource
sharedList
, which we will see in a bit.
Invoking produce method from run at thread invocation
public void run() { try { produce(); } catch (InterruptedException e) { e.printStackTrace(); } } private void produce() throws InterruptedException { .... }
5.1.1 produce method
Let’s now see what produce method does, which is where actual action for Producer lies.
- First of all our producer should not go down and keep on trying to insert produced data in
sharedList
, so we will run a while loop with true condition so that producer keeps on running.
While loop to keep producer running continuously
private void produce() throws InterruptedException { while(true) { ... } }
- Since sharedList is the shared resource and we don’t want threads to access sharedList at the same time. So all of the work will be done under a synchronized block only.
Synchronizing on shared resource
private void produce() throws InterruptedException { while(true) { synchronized (sharedList) { ... } } }
- Now if suppose,
sharedList
has reached its limit (remembermaxCount
we set?), we don’t want producer to produce any more but rather wait for consumer to consume few elements first. - So every time we loop over, we will first check if size() of the
sharedList
has reachedmaxCount
. If so then producer will invoke wait in Java on sharedList and will go in wait until consumer consumes and notify.
Wait for Consumer to consumer data if shared resource has reached capacity
private void produce() throws InterruptedException { while(true) { synchronized (sharedList) { while (sharedList.size() == maxCount) { sharedList.wait(); } } } }
- In case limit has not reached and size of sharedList is still less than
maxCount
, then producer will add one element in thesharedList
and notify any waiting consumer thread to consume fromsharedList
- We are adding
elementCount
in list incrementing it after adding to keep track of elements produced and consumed.
Method to push data into shared resource
private void produce() throws InterruptedException { while(true) { synchronized (sharedList) { while (sharedList.size() == maxCount) { sharedList.wait(); } while (sharedList.size() < maxCount) { System.out.println(Thread.currentThread().getName()+ " producing: " + elementCount); sharedList.add(elementCount++); sharedList.notifyAll(); } } } }
5.2 Consumer
We will define our Consumer class now. Here are few things to keep in mind while defining Consumer:
- It must implements Runnable as well, since we want to run Consumer in a separate thread.
- Consumer need to have access to the shared resource i.e.
sharedList
. Same has been defined as instance variable in Consumer class. - A constructor to initialize
sharedList
while creating thread.
Constructor of Consumer Class
class Consumer implements Runnable { List sharedList; public Consumer(List sharedList) { this.sharedList = sharedList; } ... }
- We will be implementing run method which calls consume method to consume element from
sharedList
Invoking consume method from run at thread invocation
public void run() { try { consume(); } catch (InterruptedException e) { e.printStackTrace(); } } private void consume() throws InterruptedException { ... }
5.2.1 consume method
- Like producer, we want our consumer thread to run infinitely so that it can keep consuming elements inserted in list. So there is a while loop with true condition to keep it running.
- Similarly, before consuming anything consumer must take lock of sharedList to avoid simultaneous access of the list.
Synchronizing shared list
private void consume() throws InterruptedException { while(true) { synchronized (sharedList) { ... } } }
- Now consumer can only consume when there is an element available in sharedList
- To validate that, consumer will first check if the size of the sharedList is 0. If it is then, consumer will just wait on sharedList till producer adds a new element in list and notify consumer thread.
- If size if not 0, which means there are elements in the sharedList, then consumer will remove the first element from list, print it and notify any thread (like producer) to continue working.
Method to consume data from shared resource
private void consume() throws InterruptedException { while(true) { synchronized (sharedList) { while (sharedList.size() == 0) { sharedList.wait(); } System.out.println(Thread.currentThread().getName()+ " consumed: " + sharedList.remove(0)); sharedList.notifyAll(); } } }
5.3 Start the Threads
- Now that we have our Producer and Consumer classes ready, we can go ahead and start these threads to see wait and notify() in action in Java.
- So we create two threads one
producerThread
and anotherconsumerThread
with name Producer and Consumer resp. - We will then start
producerThread
andconsumerThread
which will in turn call run method to start the execution. - We have kept the
maxCount
as 5 as of now.
Invocation of Producer and Consumer Threads
public static void main(String[] args) { List list = new ArrayList(); int maxCount = 5; Thread producerThread = new Thread(new Producer(list, maxCount), "Producer"); Thread consumerThread = new Thread(new Consumer(list), "Consumer"); producerThread.start(); consumerThread.start(); }
5.4 Output
Here is the output of running above code. Since maxCount
is very small, producer and consumer seems to work sequentially. You can increase the maxCount
to, let’s say, 1000 and you will notice that consumer starts consuming before producer can insert all 1000 elements in sharedList
Output of the Example
Producer producing: 0 Producer producing: 1 Producer producing: 2 Producer producing: 3 Producer producing: 4 Consumer consumed: 0 Consumer consumed: 1 Consumer consumed: 2 Consumer consumed: 3 Consumer consumed: 4 Producer producing: 5 Producer producing: 6 Producer producing: 7 Producer producing: 8 Producer producing: 9 Consumer consumed: 5 Consumer consumed: 6 Consumer consumed: 7 Consumer consumed: 8 Consumer consumed: 9
6. Summary
So this example demonstrates how wait work in Java. Also, how wait and notify can be used to set up communication between multiple threads.
7. Download the Source Code
You can download the full source code of this example here: Java Wait Example