Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected thread wakeup

I was expecting the second thread in the following example to hang, since it waits on an object with no corresponding notify. Instead, it falls through to the println, presumably due to a spurious wakeup.

public class Spurious {
    public static void main(String[] args) {

        Thread t1 = new Thread() { 
            public void run() { 
                System.out.println("Hey!"); 
            }  
        };
        Thread t2 = new Thread() { 
            public void run() 
            {
                try {
                    synchronized (t1) {
                        t1.wait();
                    }
                } catch (InterruptedException e) {
                    return;
                }
                System.out.println("Done.");
            }
        };
        t1.start();
        t2.start();
    }
}

Output:

Hey!
Done.

On the other hand, if one removes the "Hey!" println from the first thread, the second thread will indeed hang. This happens on both MacOS and Linux.

Any idea why?

like image 354
Marco Avatar asked Feb 07 '23 14:02

Marco


1 Answers

This is not a spurious wakeup, a spurious wakeup is caused by a race condition in the JVM. This is a race condition in your code.

The println keeps thread1 alive just long enough that thread2 can start waiting before thread1 terminates.

Once thread1 terminates it sends a notification to everything waiting on its monitor. thread2 receives the notification and ceases waiting.

Removing the println reduces the time needed for thread1 to finish so that thread1 has already finished by the time thread2 can start waiting on it. thread1 is no longer alive and its notification has already occurred before thread2 started waiting, so thread2 waits forever.

That threads send a notification when they die is documented in the API for Thread#join:

This implementation uses a loop of this.wait calls conditioned on this.isAlive. As a thread terminates the this.notifyAll method is invoked. It is recommended that applications not use wait, notify, or notifyAll on Thread instances.

(For a thread to call notifyAll it has to hold the lock, if the other thread grabs the lock, it can keep the terminating thread alive and delay the notifyAll until the terminating thread can acquire the lock.)

The moral (well, one of the morals) is to always wait in a loop with a condition variable, see the Oracle tutorial. If you change Thread2 to look like this:

    Thread t2 = new Thread() { 
        public void run() 
        {
            try {
                synchronized (t1) {
                    while (t1.isAlive()) {
                        t1.wait();
                    }
                }
            } catch (InterruptedException e) {
                return;
            }
            System.out.println("Done.");
        }
    };

then thread2 should exit regardless of whether thread2 can start waiting before thread1 finishes.

Of course this is total toy example territory:

  • Don't extend Thread, use Runnable or Callable.

  • Don't lock on threads.

  • Don't start Threads, use Executors.

  • Prefer higher level concurrency constructs to wait/notify.

like image 100
Nathan Hughes Avatar answered Feb 13 '23 23:02

Nathan Hughes