意外的线程唤醒

Unexpected thread wakeup

我原以为下例中的第二个线程会挂起,因为它等待一个没有相应通知的对象。相反,它落入了 println,大概是由于虚假唤醒。

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();
    }
}

输出:

Hey!
Done.

另一方面,如果从第一个线程中删除 "Hey!" println,第二个线程确实会挂起。这在 MacOS 和 Linux.

上都会发生

知道为什么吗?

您正在等待 Thread 对象。这是一种不好的做法,在 Thread 的 javadoc 中明确不鼓励这样做(Thread.join,准确地说)。

原因是当您调用 thread.join() 阻塞直到线程停止 运行 时,您实际上是在等待线程。当线程停止 运行 时,它会发出通知,以便解除对 join() 的所有调用者的阻塞。

由于您在线程上等待,当线程停止时您会隐式收到通知 运行。

这不是虚假唤醒,虚假唤醒是由 JVM 中的竞争条件引起的。这是您代码中的竞争条件。

println 使线程 1 保持活动状态的时间刚好足以使线程 2 可以在线程 1 终止之前开始等待。

线程 1 终止后,它会向等待其监视器的所有对象发送通知。 thread2 收到通知并停止等待。

删除 println 减少了线程 1 完成所需的时间,这样线程 1 已经完成,线程 2 可以开始等待它。线程 1 不再存在,并且在线程 2 开始等待之前它的通知已经发生,因此线程 2 永远等待。

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.

(调用notifyAll的线程必须持有锁,如果其他线程抢到了锁,它可以保持终止线程存活并延迟notifyAll,直到终止线程可以获取锁。)

道德(好吧,道德之一)是始终在带有条件变量的循环中等待,参见 the Oracle tutorial。如果将 Thread2 更改为如下所示:

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

那么无论线程 2 是否可以在线程 1 完成之前开始等待,线程 2 都应该退出。

当然这是玩具示例的总范围:

  • 不要扩展 Thread,使用 Runnable 或 Callable。

  • 不要锁定线程。

  • 不要启动线程,使用执行器。

  • 比wait/notify更喜欢更高级别的并发结构。