Java中的虚假唤醒真的发生了吗?

时间:2009-06-26 18:42:03

标签: java multithreading locking spurious-wakeup

看到各种与锁定相关的问题,并且(几乎)总是找到'因为虚假唤醒而导致的循环'术语 1 我想,有没有人经历过这样的唤醒(假设有一个像样的硬件/软件环境)例如)?

我知道“虚假”一词意味着没有明显的理由,但这种事件可能是什么原因?

1 注意:我不是在质疑循环练习。)

编辑:帮助问题(对于那些喜欢代码示例的人):

如果我有以下程序,我运行它:

public class Spurious {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition cond = lock.newCondition();
        lock.lock();
        try {
            try {
                cond.await();
                System.out.println("Spurious wakeup!");
            } catch (InterruptedException ex) {
                System.out.println("Just a regular interrupt.");
            }
        } finally {
            lock.unlock();
        }
    }
}

我可以做些什么才能在不等待随机事件的情况下虚假地唤醒await

7 个答案:

答案 0 :(得分:194)

维基百科article on spurious wakeups有这个小故事:

  

Linux中的pthread_cond_wait()功能是使用futex系统调用实现的。当进程收到信号时,Linux上的每个阻塞系统调用都会以EINTR突然返回。 ... pthread_cond_wait()无法重新启动等待,因为它可能会在futex系统调用之外的一小段时间内错过真正的唤醒。只有呼叫者检查不变量才能避免这种竞争条件。因此,POSIX信号将产生虚假唤醒。

摘要:如果发出Linux进程信号,则其等待的线程将各自享受一个不错的热门虚假唤醒

我买了。这是一个比通常模糊的“性能”原因更容易吞下的药丸。

答案 1 :(得分:21)

我有一个展示这种行为的生产系统。 线程等待队列中有消息的信号。 在繁忙时期,高达20%的唤醒是虚假的(即当它唤醒时,队列中没有任何内容)。 该线程是消息的唯一消费者。 它运行在Linux SLES-10 8处理器盒上,使用GCC 4.1.2构建。 这些消息来自外部源,并且是异步处理的,因为如果我的系统没有足够快地读取它们,就会出现问题。

答案 2 :(得分:13)

要回答titile中的问题 - 是的!它确实会发生。尽管Wiki article提到了关于虚假唤醒的大量信息,但我遇到的一个很好的解释是跟着 -

  

想一想......就像任何代码一样,线程调度程序可能会因底层硬件/软件中发生异常而出现临时停电。当然,应该注意尽可能少地发生这种情况,但由于没有100%强大的软件这样的事情,因此可以合理地假设这种情况发生并且如果调度程序需要注意优雅的恢复。检测到这种情况(例如通过观察丢失的心跳)。

     

现在,调度程序如何恢复,考虑到在停电期间它可能会遗漏一些旨在通知等待线程的信号?如果调度程序什么都不做,那就提到"不幸"线程只会挂起,永远等待 - 为了避免这种情况,调度程序只会向所有等待的线程发送信号。

     

这使得有必要建立一个"合同"可以无缘无故地通知等待线程。确切地说,有一个原因 - 调度程序停电 - 但由于线程被设计(有充分的理由)而没有注意到调度程序内部实现细节,因此这个原因可能更好地呈现为"虚假"。

我正在阅读Source的答案,并认为这是合理的。另请阅读

Spurious wakeups in Java and how to avoid them

PS:以上链接指向我的个人博客,其中包含有关虚假唤醒的其他详细信息。

答案 3 :(得分:9)

Cameron Purdy写了blog post一段时间,被虚假的唤醒问题击中。是的,它发生了

我猜它是在规范中(作为一种可能性),因为Java部署的某些平台的局限性?虽然我可能错了!

答案 4 :(得分:8)

只是添加这个。是的,它发生了,我花了三天时间在24核计算机(JDK 6)上搜索多线程问题的原因。 10次​​执行中有4次没有任何模式。这绝不会发生在2核或8核上。

研究了一些在线资料,这不是Java问题,而是一种罕见但预期的行为。

答案 5 :(得分:0)

https://stackoverflow.com/a/1461956/14731很好地解释了为什么即使底层操作系统没有触发虚假唤醒,也需要防止虚假唤醒。有趣的是,这种解释适用于多种编程语言,包括Java。

答案 6 :(得分:0)

回答OP的问题

我该怎么做才能唤醒这个虚假的等待,而无需永远等待 发生随机事件?

没有任何虚假唤醒可以唤醒此等待线程!

无论在特定平台上是否可能发生虚假唤醒,对于OP来说,Condition.await()肯定不可能返回并看到“虚假”行醒来!” 在输出流中。

除非您使用的是非常奇特的Java Class Library

这是因为标准OpenJDKReentrantLock的方法newCondition()返回了AbstractQueuedSynchronizer的{​​{1}}接口的实现,嵌套了{{1 }}(顺便说一下,它是此类库中Condition接口的唯一实现), ConditionObject的方法Condition本身会检查条件是否不成立,并且没有任何虚假唤醒会强制该方法错误地返回。

顺便说一句,您可以自己检查它,因为一旦涉及到基于ConditionObject的实现,就很容易模拟虚假唤醒。 await()使用低级AbstractQueuedSynchronizer的{​​{1}}和AbstractQueuedSynchronizer方法,如果您在等待LockSupport的线程上调用park,动作无法与虚假唤醒区分开。

稍微重构OP的代码段,

unpark

,无论unparking(主)线程尝试唤醒等待线程有多困难,在这种情况下,LockSupport.unpark方法将永远不会返回。

javadoc of Condition interface中讨论了Condition的等待方法的虚假唤醒。 尽管确实如此,

在等待条件时,允许进行虚假唤醒

建议应用程序程序员始终假定它们可以发生,因此总是在循环中等待。

但后来又添加了

一个实现可以免费消除虚假唤醒的可能性

public class Spurious { private static class AwaitingThread extends Thread { @Override public void run() { Lock lock = new ReentrantLock(); Condition cond = lock.newCondition(); lock.lock(); try { try { cond.await(); System.out.println("Spurious wakeup!"); } catch (InterruptedException ex) { System.out.println("Just a regular interrupt."); } } finally { lock.unlock(); } } } private static final int AMOUNT_OF_SPURIOUS_WAKEUPS = 10; public static void main(String[] args) throws InterruptedException { Thread awaitingThread = new AwaitingThread(); awaitingThread.start(); Thread.sleep(10000); for(int i =0 ; i < AMOUNT_OF_SPURIOUS_WAKEUPS; i++) LockSupport.unpark(awaitingThread); Thread.sleep(10000); if (awaitingThread.isAlive()) System.out.println("Even after " + AMOUNT_OF_SPURIOUS_WAKEUPS + " \"spurious wakeups\" the Condition is stil awaiting"); else System.out.println("You are using very unusual implementation of java.util.concurrent.locks.Condition"); } } 的{​​{1}}接口实现确实做到了-消除了任何可能的虚假唤醒

这肯定适用于其他Condition.await()的等待方法。

因此,结论是:

我们应该始终在循环中调用Condition并检查条件是否不成立,但是对于标准的OpenJDK,Java类库永远不会发生。 除非再次使用非常不寻常的Java类库(必须非常不寻常,否则是因为另一个著名的非OpenJDK Java类库,目前几乎已经消失的GNU ClasspathApache Harmony似乎具有相同的含义) AbstractQueuedSynchronizer接口的标准实现)