04 Usage Notes for Wait, Notify, Notify All Methods

04 Usage Notes for wait, notify, notifyAll Methods #

In this lesson, we will mainly learn about the precautions for using the wait/notify/notifyAll methods.

We will start with three questions:

  1. Why must the wait method be used within synchronized protected synchronized code?
  2. Why are wait/notify/notifyAll defined in the Object class, while sleep is defined in the Thread class?
  3. What are the similarities and differences between wait/notify and sleep methods?

Why must wait be used within synchronized protected synchronized code? #

Firstly, let’s look at the first question: why must the wait method be used within synchronized protected synchronous code?

Let’s first take a look at the source code comments for the wait method.

“The wait method should always be used in a loop:

synchronized (obj) {

    while (condition does not hold)

        obj.wait();

    ... // Perform action appropriate to condition

}

This method should only be called by a thread that is the owner of this object’s monitor.”

The meaning of the English part is that when using the wait method, the wait method must be placed within a synchronized protected while code block, and the execution condition must always be checked. If the condition is satisfied, continue execution, and if it is not satisfied, the wait method is executed. Before executing the wait method, the monitor lock of the object must be acquired, which is commonly referred to as the synchronized lock. So, what benefits does designing it this way bring?

Let’s think about this question in reverse. If the wait method is not required to be used within synchronized protected synchronous code and can be called freely, then it is possible to write code like this:

class BlockingQueue {

    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {

        buffer.add(data);

        notify();  // Since someone may be waiting in take

    }

    public String take() throws InterruptedException {

        while (buffer.isEmpty()) {

            wait();

        }

        return buffer.remove();

    }

}

In the code, there are two methods: the give method is responsible for adding data to the buffer, and after adding, it executes the notify method to wake up the thread waiting previously. The take method is responsible for checking if the entire buffer is empty. If it is empty, it enters the waiting state, and if not, it retrieves a piece of data. This is a typical producer-consumer approach.

However, this code is not protected by synchronized, so the following scenario can occur:

  1. First, the consumer thread calls the take method and checks if the buffer.isEmpty method returns true. If it returns true, it means that buffer is empty, and the thread wants to enter the waiting state. But before the thread calls the wait method, it is paused by the scheduler, so at this point, the wait method has not been executed.
  2. At this time, the producer starts running and executes the entire give method. It adds data to the buffer and executes the notify method. However, notify has no effect because the wait method of the consumer thread has not been executed, so no threads are waiting to be woken up.
  3. At this point, the consumer thread that was paused by the scheduler returns and continues to execute the wait method, entering the waiting state.

Although the consumer checked the buffer.isEmpty condition earlier, when the wait method is actually executed, the result of buffer.isEmpty has become stale and no longer represents the latest situation. This is because the “check-execute” process is not atomic and was interrupted in the middle, making it thread-unsafe. Assuming there are no more producers producing at this time, consumers may fall into endless waiting because they missed the wake-up call from the notify in the give method.

We can see that because the wait method is not protected by synchronized in the take method, its while loop condition check and the wait method cannot form an atomic operation, which can easily lead to program errors.

We have rewritten the code to meet the requirements of synchronized protected synchronized code blocks as specified in the comments, and the code is as follows:

public void give(String data) {
  synchronized (this) {
    buffer.add(data);
    notify();
  }
}

public String take() throws InterruptedException {
  synchronized (this) {
    while (buffer.isEmpty()) {
      wait();
    }
    return buffer.remove();
  }
}

This ensures that the notify method will never be called between buffer.isEmpty and the wait method, thus improving the program’s safety.

Additionally, the wait method releases the monitor lock, which requires us to first enter the synchronized block and hold the lock.

There is also the issue of “spurious wakeup,” where a thread may be woken up without being notified/notifyAll, interrupted, or timed out, which is not what we want to see. Although the probability of spurious wakeups occurring in actual production is very low, the program still needs to ensure correctness in the event of spurious wakeups, so a while loop structure with a condition check is needed.

while (condition does not hold)
  obj.wait();

This way, even if there is a spurious wakeup, it will check the condition inside the while loop again. If the condition is not met, it will continue to wait, eliminating the risk of spurious wakeups.

Why are wait/notify/notifyAll defined in the Object class, while sleep is defined in the Thread class? #

Let’s look at the second question: why are the wait/notify/notifyAll methods defined in the Object class while the sleep method is defined in the Thread class? There are two main reasons:

  1. Because in Java, every object has a monitor, also known as a monitor lock. Since every object can be locked, this requires a place in the object header to store lock information. This lock is at the object level, not the thread level. Wait/notify/notifyAll are also lock-level operations, and their locks belong to objects. Therefore, it is most appropriate to define them in the Object class, as the Object class is the superclass of all objects.
  2. If the wait/notify/notifyAll methods were defined in the Thread class, it would bring significant limitations. For example, a thread may hold multiple locks to implement complex coordinated logic. If the wait method were defined in the Thread class, how would we allow a thread to hold multiple locks? And how would we specify which lock the thread is waiting for? Since we want the current thread to wait for a particular object’s lock, it is natural to achieve this by operating on the object itself rather than the thread.

Similarities and Differences between wait/notify and sleep methods #

The third question compares the similarities and differences between the wait/notify and sleep methods, primarily focusing on the wait and sleep methods. Let’s start with their similarities:

  1. Both methods can make a thread block.
  2. Both methods can respond to interrupt signals: if a thread is waiting, it can respond to an interrupt signal and throw an InterruptedException.

However, they also have many differences:

  1. The wait method must be used within synchronized protected code, while the sleep method does not have this requirement.
  2. When executing the sleep method in synchronized code, the monitor lock is not released, while the wait method actively releases the monitor lock.
  3. The sleep method requires a defined time parameter and will automatically resume after the time expires. On the other hand, the parameterless wait method implies an indefinite wait until it is interrupted or awakened, and it does not automatically resume.
  4. wait/notify are methods of the Object class, while sleep is a method of the Thread class.

These are the similarities and differences between wait/notify and sleep.