Java中是否会发生虚假唤醒?


208

看到各种与锁定相关的问题,并且(几乎)总是发现“由于虚假唤醒而引起的循环”术语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将其虚假地唤醒,而无需永远等待随机事件?


1
对于在POSIX系统上运行并使用pthread_cond_wait()真正问题的JVM:“为什么pthread_cond_wait会有虚假的唤醒?”
流量

Answers:


204

维基百科上有关虚假唤醒的文章有以下提示:

pthread_cond_wait()Linux中的功能是使用futex系统调用实现的。EINTR当进程接收到信号时,Linux上的每个阻塞系统调用都会突然返回。... pthread_cond_wait()无法重新开始等待,因为它可能会在futex系统调用之外的短时间内错过一次真正的唤醒。只能通过调用方检查不变性来避免这种竞争状态。因此,POSIX信号将产生伪唤醒。

简介:如果发出Linux进程信号,则其等待线程将各自享受良好的热虚假唤醒

我买它。相比通常含糊的“出于性能”的理由,这是容易吞咽的药丸。


13
更好的解释在这里:stackoverflow.com/questions/1461913/…–
Gili

3
对于Unix派生系统中的所有阻塞系统调用,此EINTR解除阻塞均适用。这使内核简化了很多,但是应用程序程序员负担了重担。
Tim Williscroft 2011年

2
我以为pthread_cond_wait()和朋友不能返回EINTR,但是如果虚假唤醒就返回零?来自: pubs.opengroup.org/onlinepubs/7908799/xsh/… “这些功能不会返回[EINTR]的错误代码。”
gubby 2014年

2
@jgubby是的。基础futex()调用返回EINTR,但是该返回值不会冒泡上升到下一个级别。因此,pthread调用者必须检查不变性。他们的意思是,pthread_cond_wait()返回时必须再次检查循环条件(不变),因为等待可能已经被虚假地唤醒了。在系统调用期间接收信号是一种可能的原因,但不是唯一的原因。
约翰·库格曼

1
大概,该pthread库可以提供自己的不变性和检查逻辑,以便消除虚假的唤醒,而不是将责任转移给用户。这(可能)会对声称的性能产生影响。

22

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


15

要在标题中回答问题- 是的!尽管Wiki文章提到了很多有关虚假唤醒的信息,但对于我所遇到的相同情况,一个很好的解释如下:

试想一下……就像任何代码一样,线程调度程序可能会由于底层硬件/软件中发生的异常而经历暂时的中断。当然,应该尽量避免这种情况的发生,但是由于没有100%健壮的软件之类的东西,因此可以合理地假设这种情况可能发生,并在调度程序检测到这种情况时谨慎进行恢复(例如通过观察丢失的心跳)。

现在,考虑到在停电期间它可能会丢失一些旨在通知等待线程的信号,调度程序如何恢复?如果调度程序什么也不做,则提到的“不幸”线程将永远挂起,一直等待-为避免这种情况,调度程序将向所有正在等待的线程发送信号。

这使得必须建立一个“契约”,以便可以毫无理由地通知等待线程。确切地说,这是有原因的-调度程序中断-但由于线程(出于充分的理由)被设计为不了解调度程序内部实现的详细信息,因此,此原因很可能表现为“虚假的”。

我从Source阅读此答案,并发现它足够合理。也看了

Java中的虚假唤醒以及如何避免它们

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


9

卡梅隆·珀迪Cameron Purdy)不久前写了一篇博客文章,内容涉及虚假唤醒问题。是的,它发生了

我猜它在规范中(可能)是因为Java部署在某些平台上的限制?虽然我可能错了!


我阅读了这篇文章,并给了我一个关于通过随机/确定性唤醒应用程序来测试一个应用程序是否符合循环等待范式的想法。还是已经在某处可用?
akarnokd

关于SO的另一个问题是:“是否存在可以用于测试的严格 VM?”。我很想看到一个严格的线程本地存储-我不认为他们还不存在
oxbow_lakes

8

只是添加此。是的,它发生了,我花了三天时间在24核计算机(JDK 6)上寻找导致多线程问题的原因。10个死刑犯中有4个遇到了这种情况,没有任何模式。这在2核或8核上从未发生过。

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


您好ReneS,您是否正在开发在那里运行的应用程序?是否有(wait)方法在循环检查外部条件时调用了wait()方法,如java docdocs.oracle.com/javase/6/docs/api/java/lang/…中所建议的那样
gumkins

我写了关于它的文章,是的,解决方案是带有条件检查的while循环。我的错误是缺少循环...但是,我了解了这些唤醒...从来没有在两个内核上,通常是在24cores上blog.xceptance.com/2011/05/06/spurious-wakeup-the-rare-event
ReneS

在40核以上的Unix服务器上运行应用程序时,我也有类似的经历。它有大量的虚假唤醒。-因此,似乎虚假唤醒的数量与系统处理器核心的数量成正比。
bvdb


0

回答OP的问题

我该怎么做才能虚假地唤醒等待,而不必永远等待随机事件?

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

不管虚假唤醒可以或不可以发生在特定的平台,在OP的的情况下片断是积极不可能Condition.await()返回,看看行“虚假唤醒!” 在输出流中。

除非您使用非常特殊的Java类库

这是因为OpenJDK的标准ReentrantLock方法newCondition()返回嵌套AbstractQueuedSynchronizerCondition接口的实现ConditionObject(顺便说一句,它是Condition此类库中唯一的接口实现),而ConditionObject方法await()本身检查条件是否不保持,没有任何虚假的唤醒会强制此方法错误地返回。

顺便说一下,您可以自己检查它,因为一旦AbstractQueuedSynchronizer涉及到基于-的实现,就很容易模拟伪唤醒。 AbstractQueuedSynchronizer使用低级LockSupportparkunpark方法,并且如果您LockSupport.unpark在等待的线程上调用Condition,则此操作无法与伪唤醒区分开。

稍微重构OP的代码段,

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

,并且无论unparking(main)线程尝试唤醒等待线程的努力程度如何,Condition.await()在这种情况下该方法都不会返回。

接口ConditionjavadocCondition中将讨论on 的等待方法的虚假唤醒。尽管确实如此,

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

然后

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

但后来补充说

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

和接口AbstractQueuedSynchronizer的实现Condition确实做到了这一点- 消除了任何可能的虚假唤醒

这对于其他ConditionObject的等待方法肯定适用。

因此,结论是:

我们应该始终Condition.await在循环中进行调用,并检查条件是否不成立,但是对于标准的OpenJDK,Java类库是永远不会发生的。除非再次使用非常不寻常的Java类库(这一定是非常不寻常的,因为另一个众所周知的非OpenJDK Java类库,目前几乎已经绝种的GNU ClasspathApache Harmony似乎与Condition接口的标准实现相同)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.