Java中是否有一个“直到条件变为真”的功能?


76

我正在为服务器编写侦听器线程,此刻我正在使用:

while (true){
    try {
        if (condition){
            //do something
            condition=false;
        }
        sleep(1000);

    } catch (InterruptedException ex){
        Logger.getLogger(server.class.getName()).log(Level.SEVERE, null, ex);
    }
}

使用上面的代码,我遇到了运行功能吃掉所有cpu时间循环的问题。睡眠功能有效,但似乎是临时解决方案,而不是解决方案。

是否有一些函数会阻塞直到变量“ condition”变为“ true”?还是连续循环等待变量值改变的标准方法?


3
上面的代码为什么会耗尽您的所有cpu,看来每秒只会启动一次。反正看到这个线程:stackoverflow.com/questions/289434/...
jontro

3
有关此主题的完整介绍,请参见《Java并发实践》第14章。但更普遍,你可能需要使用更高级别的实用工具,比如BlockingQueueSemaphoreCountDownLatch而不是低层次的机制。
Brian Goetz

Answers:


68

这样的轮询绝对是最不受欢迎的解决方案。

我假设您有另一个线程将做一些事情使该条件成立。有几种同步线程的方法。在您的情况下,最简单的方法是通过对象进行通知:

主线程:

synchronized(syncObject) {
    try {
        // Calling wait() will block this thread until another thread
        // calls notify() on the object.
        syncObject.wait();
    } catch (InterruptedException e) {
        // Happens if someone interrupts your thread.
    }
}

其他线程:

// Do something
// If the condition is true, do the following:
synchronized(syncObject) {
    syncObject.notify();
}

syncObject本身可以很简单Object

线程间通信还有许多其他方式,但是要使用哪种方式则取决于您正在做什么。


1
不客气!请记住,还有其他同步方式,例如信号量,阻塞队列等……这完全取决于您要执行的操作。对象是很棒的通用线程同步工具。祝您的应用程序好运!
EboMike

12
尝试捕获应该包装在一个循环中,以测试实际的潜在状况,以防止虚假的唤醒(请参阅wait doco)。
劳伦斯·多尔

10
值得注意的是,如果首先调用notifyAll,即使条件在开始等待之前就已经满足,wait()将永远等待。
彼得·劳瑞

3
由于java.concurent已经问世,所以这个答案已经过时了。更干净,更不易出错的等待方式是根据有效的Java Ed 2
Alan Berezin

1
@PeterLawrey还应该指出(给出此答案后甚至超过八年),如果另一个线程也开始以这种方式等待,则使用notfify而不是notifyAll会导致有趣的效果,因为notify仅通知了一个等待线程(假设随机)。
Lothar

53

EboMike的答案Toby的答案都在正确的轨道上,但是它们都有致命的缺陷。该缺陷称为丢失通知

问题是,如果一个线程调用foo.notify(),它将根本不做任何事情,除非某个其他线程已经在foo.wait()调用中处于休眠状态。对象,foo不记得它已收到通知。

有一个原因导致不允许您调用,foo.wait()或者foo.notify()除非线程在foo上同步。这是因为避免丢失通知的唯一方法是使用互斥量保护条件。完成后,它看起来像这样:

使用者线程:

try {
    synchronized(foo) {
        while(! conditionIsTrue()) {
            foo.wait();
        }
        doSomethingThatRequiresConditionToBeTrue();
    }
} catch (InterruptedException e) {
    handleInterruption();
}

生产者线程:

synchronized(foo) {
    doSomethingThatMakesConditionTrue();
    foo.notify();
}

更改条件的代码和检查条件的代码都在同一对象上同步,并且使用者线程在等待之前显式测试条件。wait()当条件为真时,用户无法错过通知并永远陷入通话中。

另请注意,这wait()是一个循环。这是因为,在一般情况下,当使用者重新获取foo锁并唤醒时,其他一些线程可能再次使条件变为假。即使在您的程序中不可能做到这一点,在某些操作系统中,foo.wait()即使foo.notify()没有被调用也可能返回。这就是所谓的“伪唤醒”,它是允许发生的,因为它使等待/通知在某些操作系统上更容易实现。


1
我们应该将try-catch放在while循环之内还是之外?推荐哪种方式,为什么?
Jaydev '16

1
@JaydevKalivarapu,假设您要询问的是InterruptedException吗?由您决定中断的含义,但是在大多数情况下,它可能意味着“停止等待”并执行其他操作(例如,关闭整个程序。)因此,在大多数情况下,您会想要它就像我上面的例子一样,中断处理程序位于循环之外。
所罗门慢

6
@ JaydevKalivarapu,PS:回到上面的答案时,我还不知道该模式有一个名称:Oracle Java教程将其称为保护块。您可以在docs.oracle.com/javase/tutorial/essential/concurrency/…上
Solomon Slow

1
@JianGuo,如果未锁定foo.wait()则将抛出。这是为了提醒您,对于不持有锁的代码,这没有任何意义。上面我的答案触及了原因,但是,如果您需要全面的解释,则应该阅读本教程。docs.oracle.com/javase/tutorial/essential/concurrency/...IllegalMonitorStateExceptionfoowait()
索罗门慢

1
@JianGuo,如果这样做,那么可能会发生;(1)使用者测试条件并发现它为假,(2)使用者尝试输入synchronized(foo)但由于生产者已经在上进行了同步而被阻止foo,(3)生产者导致条件变为真,调用foo.notify()和然后释放锁,(4)使用者进入该synchronized(foo)块并调用foo.wait()。现在,消费者被困在等待永远不会到达的通知。有时将此问题称为“丢失通知”。
所罗门慢

25

由于没有人使用CountDownLatch发布解决方案。关于什么:

public class Lockeable {
    private final CountDownLatch countDownLatch = new CountDownLatch(1);

    public void doAfterEvent(){
        countDownLatch.await();
        doSomething();
    }

    public void reportDetonatingEvent(){
        countDownLatch.countDown();
    }
}

21

与EboMike的答案类似,您可以使用类似于wait / notify / notifyAll的机制,但要准备好使用Lock

例如,

public void doSomething() throws InterruptedException {
    lock.lock();
    try {
        condition.await(); // releases lock and waits until doSomethingElse is called
    } finally {
        lock.unlock();
    }
}

public void doSomethingElse() {
    lock.lock();
    try {
        condition.signal();
    } finally {
        lock.unlock();
    }
}

您将等待其他线程通知的某种情况(在本例中为doSomethingElse),此时,第一个线程将继续...

Lock在内部同步上使用s有很多优点,但是我只喜欢使用一个显式Condition对象来表示条件(您可以拥有多个对象,这对于生产者-消费者这样的事物来说是很好的选择)。

另外,我不禁注意到您在示例中如何处理被中断的异常。您可能不应该使用这种异常,而应使用重置中断状态标志Thread.currentThread().interrupt

这是因为如果引发异常,则中断状态标志将被重置(它的意思是“我不再记得被打扰,如果有人问我,我将无法告诉其他人我已经去过”),并且可能有另一个过程依靠这个问题。这个例子是其他东西基于这个this子实施了中断策略。另一个示例可能是您的中断策略,而while(true)不是已被实施为while(!Thread.currentThread().isInterrupted()(这也会使您的代码更加……具有社会影响力)。

因此,总而言之,Condition当您要使用a时Lock,using相当于使用wait / notify / notifyAll ,日志记录是邪恶的,吞咽InterruptedException是顽皮的;)


2
使用Condition+Lock等同于Object同步方法+ synchronized。前者允许在条件等待之前发出通知-另一方面,如果您Object.notify()在之前调用Object.wait(),则该线程将永远阻塞。此外,await()必须循环调用,请参阅文档。
TheOperator'Mar

@TheOperator关于“前者允许在等待条件之前发出通知”-我通过Javadoc进行了Condition查找,但找不到支持该声明的文本。你能解释一下你的陈述吗?
Nayuki '17

1
示例代码是错误的,需要在循环中调用await。有关条件,请参阅API文档。
内森·休斯

7

您可以使用信号量

当不满足条件时,另一个线程获取信号量。
您的线程将尝试使用acquireUninterruptibly()
或来获取它,tryAcquire(int permits, long timeout, TimeUnit unit)并且将被阻止。

当满足条件时,信号量也会被释放,您的线程将获取它。

您也可以尝试使用SynchronousQueueCountDownLatch


4

无锁解决方案(?)

我遇到了同样的问题,但是我想要一个不使用锁的解决方案。

问题:我最多只有一个线程从队列中消耗。多个生产者线程不断插入队列中,并且需要在等待时通知使用者。该队列是无锁的,因此使用锁进行通知会导致生产者线程不必要的阻塞。每个生产者线程都需要先获取锁,然后才能通知正在等待的使用者。我相信我想出了使用LockSupport和的无锁解决方案AtomicReferenceFieldUpdater。如果JDK中存在无锁屏障,则找不到它。双方CyclicBarrierCoundDownLatch使用从我能找到的内部锁定。

这是我的缩写代码。只需清楚一点,此代码将只允许一个线程等待。通过使用某种类型的原子集合来存储多个所有者(ConcurrentMap可以工作),可以对其进行修改以允许多个等待者/消费者。

我已经使用此代码,它似乎可以工作。我没有对其进行广泛的测试。我建议您LockSupport在使用前阅读文档。

/* I release this code into the public domain.
 * http://unlicense.org/UNLICENSE
 */

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.LockSupport;

/**
 * A simple barrier for awaiting a signal.
 * Only one thread at a time may await the signal.
 */
public class SignalBarrier {
    /**
     * The Thread that is currently awaiting the signal.
     * !!! Don't call this directly !!!
     */
    @SuppressWarnings("unused")
    private volatile Thread _owner;

    /** Used to update the owner atomically */
    private static final AtomicReferenceFieldUpdater<SignalBarrier, Thread> ownerAccess =
        AtomicReferenceFieldUpdater.newUpdater(SignalBarrier.class, Thread.class, "_owner");

    /** Create a new SignalBarrier without an owner. */
    public SignalBarrier() {
        _owner = null;
    }

    /**
     * Signal the owner that the barrier is ready.
     * This has no effect if the SignalBarrer is unowned.
     */
    public void signal() {
        // Remove the current owner of this barrier.
        Thread t = ownerAccess.getAndSet(this, null);

        // If the owner wasn't null, unpark it.
        if (t != null) {
            LockSupport.unpark(t);
        }
    }

    /**
     * Claim the SignalBarrier and block until signaled.
     *
     * @throws IllegalStateException If the SignalBarrier already has an owner.
     * @throws InterruptedException If the thread is interrupted while waiting.
     */
    public void await() throws InterruptedException {
        // Get the thread that would like to await the signal.
        Thread t = Thread.currentThread();

        // If a thread is attempting to await, the current owner should be null.
        if (!ownerAccess.compareAndSet(this, null, t)) {
            throw new IllegalStateException("A second thread tried to acquire a signal barrier that is already owned.");
        }

        // The current thread has taken ownership of this barrier.
        // Park the current thread until the signal. Record this
        // signal barrier as the 'blocker'.
        LockSupport.park(this);
        // If a thread has called #signal() the owner should already be null.
        // However the documentation for LockSupport.unpark makes it clear that
        // threads can wake up for absolutely no reason. Do a compare and set
        // to make sure we don't wipe out a new owner, keeping in mind that only
        // thread should be awaiting at any given moment!
        ownerAccess.compareAndSet(this, t, null);

        // Check to see if we've been unparked because of a thread interrupt.
        if (t.isInterrupted())
            throw new InterruptedException();
    }

    /**
     * Claim the SignalBarrier and block until signaled or the timeout expires.
     *
     * @throws IllegalStateException If the SignalBarrier already has an owner.
     * @throws InterruptedException If the thread is interrupted while waiting.
     *
     * @param timeout The timeout duration in nanoseconds.
     * @return The timeout minus the number of nanoseconds that passed while waiting.
     */
    public long awaitNanos(long timeout) throws InterruptedException {
        if (timeout <= 0)
            return 0;
        // Get the thread that would like to await the signal.
        Thread t = Thread.currentThread();

        // If a thread is attempting to await, the current owner should be null.
        if (!ownerAccess.compareAndSet(this, null, t)) {
            throw new IllegalStateException("A second thread tried to acquire a signal barrier is already owned.");
        }

        // The current thread owns this barrier.
        // Park the current thread until the signal. Record this
        // signal barrier as the 'blocker'.
        // Time the park.
        long start = System.nanoTime();
        LockSupport.parkNanos(this, timeout);
        ownerAccess.compareAndSet(this, t, null);
        long stop = System.nanoTime();

        // Check to see if we've been unparked because of a thread interrupt.
        if (t.isInterrupted())
            throw new InterruptedException();

        // Return the number of nanoseconds left in the timeout after what we
        // just waited.
        return Math.max(timeout - stop + start, 0L);
    }
}

为了给出模糊的用法示例,我将采用james large的示例:

SignalBarrier barrier = new SignalBarrier();

使用者线程(单数,而不是复数!):

try {
    while(!conditionIsTrue()) {
        barrier.await();
    }
    doSomethingThatRequiresConditionToBeTrue();
} catch (InterruptedException e) {
    handleInterruption();
}

生产者线程:

doSomethingThatMakesConditionTrue();
barrier.signal();
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.