Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In Java, we have a wait()/notify() API. This API is one of the ways to synchronize between threads. In order to use the methods of this API, the current thread must own the monitor of the callee.

In this tutorial, we’ll explore the reasons why this requirement makes sense.

2. How wait() Works

First, we need to briefly talk about how wait() works in Java. In Java, in accordance with JLS, each object has a monitor. Essentially, it means that we can synchronize on any object that we like. It was probably not a good decision, but this is what we have now.

Having that, when we call wait(), we implicitly do two things. First, we place the current thread into the JVM internal wait set for this object monitor. The second is that once the thread is in wait set, we (or the JVM, for that matter) release the synchronization lock on this object. Here, we need to clarify – the word this means the object on which we call the wait() method.

And then, the current thread just waits in the set until another thread calls notify()/notifyAll() on this object.

3. Why Is Monitor Ownership Required?

In the previous section, we saw that the second thing JVM does is the release of the synchronization lock on this object. In order to release it, we obviously need to own it first. The reasoning for this is relatively simple: synchronization on wait() comes as a requirement in order to avoid the lost wake-up problem. This problem essentially represents a condition where we have a waiting thread that has missed the notify signal. It mostly happens due to the race condition between threads. Let us emulate this problem with an example.

Suppose we have the following Java code:

private volatile boolean jobIsDone;

private Object lock = new Object();

public void ensureCondition() {
    while (!jobIsDone) {
        try {
            lock.wait();
        } 
        catch (InterruptedException e) {
            // ...
        }
    }
}

public void complete() {
    jobIsDone = true;
    lock.notify();
}

A quick note – this code will fail in runtime with IllegalMonitorStateException. This is because, in both methods, we do not ask for a lock object monitor before wait()/notify() calls. Thus, this code is purely for demonstration and learning purposes.

Also, let’s assume we have two threads. So, thread B is doing the useful work. Once it is done, thread B needs to call the complete() method to signal the completion. We also have another thread, A, that is waiting for the job performed by B to be completed. Thread A makes its check for condition by calling the ensureCondition() method. The check for the condition is happening in the loop because of the spurious wake-up problem that occurs on the Linux kernel level, but that is another topic.

4. The Problem of the Lost Wake-up

Let’s break down our example step by step. Assume thread A called ensureCondition() and enters the while loop. It checked for a condition, which appeared to be false, so it entered the try block. Because we operate in a multithreaded environment, another thread B can simultaneously enter the complete() method. Therefore, B can call set volatile flag jobIsDone to true and call notify() before thread A called wait().

In this case, if thread B will never enter the complete() again, thread A will wait forever, and therefore, all of the resources associated with it will also live forever. This leads not only to deadlocks if thread A happens to hold another lock but to memory leaks because objects reachable from thread A stack frames will remain alive. This is because thread A is considered to be alive, and it can resume its execution. Therefore, GC is not allowed to garbage collect objects allocated in methods of A stack.

5. Solution

So, in order to avoid this condition, we need synchronization. Therefore, the caller must own the monitor of the callee before execution. So, let’s rewrite our code having synchronization concerns in mind:

private volatile boolean jobIsDone;
private final Object lock = new Object();

public void ensureCondition() {
    synchronized (lock) {
        while (!jobIsDone) {
            try {
                lock.wait();
            } 
            catch (InterruptedException e) { 
                // ...
            }
        }
    }
}

public void complete() {
    synchronized (lock) {
        jobIsDone = true;
        lock.notify();
    }
}

Here, we just added a synchronized block, where we try to acquire the lock object monitor before invoking the wait()/notify() API. Now, we avoid lost wake-up if B executes complete() method before A will invoke wait(). This is because the complete() method can be executed by B only if A has not acquired the lock object monitor. Thus, A cannot check a condition while the complete() method is executing.

6. Conclusion

In this article, we discussed why the Java wait() method requires synchronization. We need ownership of the callee monitor in order to avoid lost wake-up anomaly. If we do not do that, the JVM will take a fail-fast approach and throw IllegalMonitorStateException.

As always, the source code for these examples can be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.