Search code examples
javamultithreadingconcurrencyjvmjava.util.concurrent

Why calling notify in Java requires holding the lock?


Thread thread = new Thread(() -> {
    synchronized (this){
        try {
            this.wait();
            System.out.println("Woke");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

});
thread.start();
TimeUnit.SECONDS.sleep(1);
this.notify();

When calling notify it says

java.lang.IllegalMonitorStateException: current thread is not owner

The typical usage of notify is that you call it and then you release the lock implicitly (by leaving the synchronized block) so that the waiting threads may re-acquire the lock.

But code above calls notify even before it has the lock, so other threads can just try to acquire the lock, why not? I think the holding the lock is not necessary.


Solution

  • I think the holding the lock is not necessary.

    It is necessary because the javadoc for Object.notify() says it is necessary. It states:

    "This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:

    1. By executing a synchronized instance method of that object.
    2. By executing the body of a synchronized statement that synchronizes on the object.
    3. For objects of type Class, by executing a synchronized static method of that class."

    But your real question is why is it necessary? Why did they design it this way?

    To answer that, we need to understand that Java's wait / notify mechanism is primarily designed for implementing condition variables. The purpose of a condition variable is to allow one thread to wait for a condition to become true and for another thread to notify it that this has occurred. The basic pattern for implementing condition variables using wait() / notify() is as follows:

    // Shared lock that provides mutual exclusion for 'theCondition'.
    final Object lock = new Object();
    
    // Thread #1
    synchronized (lock) {
        // ...
        while (! theCondition) {  // One reason for this loop will
                                  // become later ...
            lock.wait();
        }
        // HERE
    }
    
    // Thread # 2
    synchronized (lock) {
        // ...
        if (theCondition) {
            lock.notify();
        }
    }
    

    This when thread #1 reaches // HERE, it knows that theCondition is now true. Furthermore it is guaranteed the current values variables that make up the condition, and any others controlled by the lock monitor will now be visible to thread #1.

    But one of the prerequisites for this actually working is that both thread #1 and thread #2 are synchronized on the same monitor. That will guarantee the visibility of the values according to a happens before analysis based on the Java Memory Model (see JLS 17.4).

    A second reason that the above needs synchronization is because thread #1 needs exclusive access to the variables to check the condition and then use them. Without mutual exclusion for the shared state between threads #1 and #2, race conditions are possible that can lead to a missed notification.

    Since the above only works reliably when threads #1 and #2 hold the monitor when calling wait and notify, the Java designers decided to enforce this in implementations of the wait and notify methods themselves. Hence the javadoc that I quoted above.


    Now ... your use-case for wait() / notify() is simpler. No information is shared between the two threads ... apart from the fact that the notify occurred. But it is still necessary to follow the pattern above.

    Consider the consequences of this caveat in the javadoc for the wait() methods:

    "A thread can wake up without being notified, interrupted, or timing out, a so-called "spurious wakeup". While this will rarely occur in practice, applications must guard against it ..."

    So one issue is that a spurious wakeup could cause the child thread to be woken before the main thread's sleep(...) completes.

    A second issue is that is the child thread is delayed, the main thread may notify the child before the child has reached the wait. The notification then be lost. (This might happen due to system load.)

    What these issues mean is that your example is incorrect ... in theory, if not in reality. And in fact, it is not possible to solve your problem using wait / notify without following the pattern above/

    A corrected version of your example (i.e. one that is not vulnerable to spurious wakeups, and race conditions) looks like this:

    final Object lock = new Object;
    boolean wakeUp = false;
    
    Thread thread = new Thread(() -> {
        synchronized (lock){
            try {
                while (!wakeUp) {
                    this.wait();
                }
                System.out.println("Woke");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    
    });
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    synchronized (lock) {
        wakeUp = true;
        this.notify();
    }
    

    Note that there are simpler and more obviously correct ways to do this using various java.concurrent.* classes.