Hurry! Try our new Interactive Courses for FREE. 🥳   🚀

Java Wait and Notify

Multithreading is the process of running multiple threads at the same time. Multithreading increases the efficiency of our code, as multiple threads divide the problem and work on the smaller parts simultaneously. However, multiple threads may need access to a single common resource.

A lock is used on this common resource to avoid parallel modification. Inconsistencies arise when the threads are not synchronized to work with this lock. The wait() and notify() methods are used for thread communication and synchronization in Java.

Let's learn more about these methods.

Java wait() Method

The Object class in Java has a final wait() method. It is used to suspend the execution of a thread. It will also make the thread give up its lock so that some other thread can access the critical section. The part of the code that modifies the shared resource is called the critical section.

This method has three overloaded signatures.

  • wait()
  • wait(long timeout)
  • wait(long timeout, int nanoseconds)

If we specify a timeout duration, then the thread will automatically wake up after the timeout. If no timeout has been mentioned, then the thread must wait for some other thread to call notify on it.

The general syntax of using the wait() method for synchronization is shown below.

synchronized(object)
{ 
    while(condition is false)
    { 
        object.wait();
    }
     
    //do the task
}

Java notify() Method

The notify() method also belongs to the Object class. This method is used to wake up a waiting thread. If multiple threads are waiting, then a randomly chosen thread is woken up.

Note that this method will not make the calling(or notifying) thread give up its lock.

Java notifyAll() Method

The notifyAll() method is very similar to the notify() method, but instead of waking just a single thread, it will wake up all the waiting threads.

Why wait() is enclosed in a while loop?

The wait() method should be enclosed in a while loop because a thread can be wake up without the notify() call. This is known as a spurious wakeup. Another reason for the while loop is that an evil thread may call notifyAll() and wake up all the waiting threads.

Threads need to check the condition and if the condition is not satisfied, then they should continue to wait.

Example: Producer and Consumer Problem

Let's understand how to use wait() and notify() with the help of an example. We will solve the traditional producer-consumer problem by using these methods. Let's first understand the producer-consumer problem.

  • This problem involves a producer and a consumer.
  • The producer will produce items and add them to a buffer.
  • The consumer will consume the produced items from the buffer.
  • The producer should not produce new items if the buffer is full.
  • The consumer should not consume if the buffer is empty.

Let's write the code for the producer class. This class will implement the runnable interface. It will have a single member called the buffer.

When the buffer is full, the producer should call the wait() method. The maximum capacity of the buffer is set to 5 elements. After producing a new item and adding it to the buffer, it should notify the consumer thread. If the consumer thread is waiting, then it will wake up and start consuming.

class Producer implements Runnable
{
	//buffer to store the produced items
	private final LinkedList<Integer> buffer;	
	Producer(LinkedList<Integer> buffer)
	{
		this.buffer = buffer;
	}
	@Override
	public void run()
	{		
		//Infinitely produce items
		while(true)
		{
			try {
			this.produce();
			}
			catch(Exception e) {
				System.out.print(e);
			}
		}
	}	
	public void produce() throws InterruptedException
	{
		synchronized(buffer)
		{
			//If the buffer is full then wait
			while(buffer.size() == 5)
			{
				System.out.println("Producer is waiting");
				buffer.wait();
			}			
			//Produce a new random number
			Random r = new Random();
			int num = r.nextInt(100);
			System.out.println("Producer produced: " + num);
			buffer.add(num);
			buffer.notifyAll();
			Thread.sleep(10);
		}
	}
}

The Consumer class should wait if the buffer is empty. When it acquires the lock, the consumer should pop the first element from the buffer and consume it. When it is done, the consumer should notify the producer thread.

class Consumer implements Runnable
{
	//buffer to consume items from
	private final LinkedList<Integer> buffer;	
	Consumer(LinkedList<Integer> buffer)
	{
		this.buffer = buffer;
	}	
	@Override
	public void run()
	{
		//Infinitely consume
		while(true)
		{
			try {
				this.consume();
			}
			catch(Exception e) {
				System.out.print(e);
			}
		}
	}	
	public void consume() throws InterruptedException
	{
		synchronized(buffer)
		{
			//Wait if the buffer is empty
			while(buffer.size() == 0)
			{
				System.out.println("Consumer is waiting");
				buffer.wait();
			}
			
			//Consume the first item from the buffer
			int num = buffer.remove(0);
			System.out.println("Consumer consumed: " + num);
			buffer.notifyAll();
			Thread.sleep(5);
		}
	}
}

Note that we use synchronized blocks to make sure that only one thread can access the buffer at a time.

A demonstration of the above methods is shown below.

public class WaitNotifyDemo
{
	public static void main(String args[])
	{
		LinkedList<Integer> buffer = new LinkedList<>();
		Producer producer = new Producer(buffer);
		Consumer consumer = new Consumer(buffer);
		
		Thread p = new Thread(producer);
		Thread c = new Thread(consumer);
		
		p.start();
		c.start();	
	}
}


Producer produced: 26
Producer produced: 37
Consumer consumed: 26
Consumer consumed: 37
Consumer is waiting
Producer produced: 66
Producer produced: 91
Producer produced: 51
Producer produced: 88
Producer produced: 61
Producer is waiting

Summary

The wait(), notify() and notifyAll() methods are used for thread communication and synchronization.

In this tutorial, we learned how to use these methods. We also learned how to solve the producer-consumer problem using these methods. Note that these are traditional methods and are a bit more complex to use than the newer APIs.