在java中有一个'阻止直到条件变为真'的功能吗?

Rol*_*lan 66 java multithreading block

我正在为服务器编写一个监听器线程,目前我正在使用:

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

    } catch (InterruptedException ex){
        Logger.getLogger(server.class.getName()).log(Level.SEVERE, null, ex);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

是否有一些函数会阻塞,直到变量'condition'变为'true'?或者是不断循环标准的等待方法,直到变量的值发生变化?

Ebo*_*ike 59

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

我假设你有另一个线程可以做一些事情来使条件成立.有几种方法可以同步线程.在您的情况下最简单的是通过对象的通知:

主线程:

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.
    }
}
Run Code Online (Sandbox Code Playgroud)

其他线程:

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

syncObject本身可以很简单Object.

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

  • try catch应该包含在一个循环中,测试真正的潜在条件以防止虚假唤醒(参见等待doco). (10认同)
  • 值得注意的是,如果先调用notifyAll,则wait()将永远等待,即使条件在开始等待之前已满足. (9认同)
  • 不客气!请记住,还有其他同步方法,例如信号量、阻塞队列等……这完全取决于您想要做什么。对象是很好的通用线程同步工具。祝你的应用程序好运! (3认同)
  • 自java.concurent问世以来,这个答案已经过时了.更清晰且不易出错的等待方式是使用CountDownLatch,每个Effective Java Ed 2 (2认同)
  • @PeterLawrey还应该注意(甚至在给出这个答案八年多之后),如果另一个线程也开始以这种方式等待,使用“notfify”而不是“notifyAll”可能会导致有趣的效果,因为“notify”只通知等待线程之一(假设它是随机的)。 (2认同)

Sol*_*low 43

EboMike的答案Toby的答案都在正确的轨道上,但它们都有一个致命的缺陷.该漏洞被称为丢失通知.

问题是,如果一个线程调用foo.notify(),它将不会做任何事情,除非一些其他线程已经在foo.wait()呼叫中休眠.该对象foo不记得它已被通知.

有一个原因,你不允许你打电话foo.wait()foo.notify()除非线程在foo上同步.这是因为避免丢失通知的唯一方法是使用互斥锁来保护条件.当它完成正确时,它看起来像这样:

消费者主题:

try {
    synchronized(foo) {
        while(! conditionIsTrue()) {
            foo.wait();
        }
        doSomethingThatRequiresConditionToBeTrue();
    }
} catch (InterruptedException e) {
    handleInterruption();
}
Run Code Online (Sandbox Code Playgroud)

制片人主题:

synchronized(foo) {
    doSomethingThatMakesConditionTrue();
    foo.notify();
}
Run Code Online (Sandbox Code Playgroud)

更改条件的代码和检查条件的代码都在同一对象上同步,并且使用者线程在等待之前显式测试条件.wait()当条件已经为真时,消费者无法错过通知并最终在通话中永久停留.

另请注意,wait()它处于循环中.这是因为,在一般情况下,当消费者重新获得foo锁定并唤醒时,其他一些线程可能会再次使该条件成为错误.即使你的程序中不可能,在某些操作系统中,foo.wait()即使foo.notify()没有被调用,也可以返回.这被称为虚假唤醒,它被允许发生,因为它使等待/通知更容易在某些操作系统上实现.

  • @JaydevKalivarapu,PS:当我写上面的答案时,我不知道那个模式有一个名字:Oracle Java教程称之为_guarded block_.你可以在https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html上阅读它. (6认同)
  • @JaydevKalivarapu,假设您正在询问“InterruptedException”,对吗?由您决定中断的含义,但在大多数情况下,它可能意味着“停止等待”并执行其他操作(例如,关闭整个程序)。因此,在大多数情况下,您会需要它就像我上面的例子一样,中断处理程序位于循环之外。 (3认同)

Tob*_*oby 18

与EboMike的答案类似,您可以使用类似于wait/notify/notifyAll的机制,但是可以使用a 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

在等待由另一个线程(在这种情况下调用doSomethingElse)通知的某些条件的情况下,第一个线程将继续......

使用Locks over内部同步有很多优点,但我更喜欢有一个显式Condition对象来表示条件(你可以拥有多个,这对于像producer-consumer这样的东西来说是个不错的选择).

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

这是因为如果抛出异常,中断状态标志将被重置(它说" 我不再记得被打断,我不能告诉其他人,如果他们要求我去过那里 ")并且另一个过程可能依赖于这个问题.例如,其他东西已经实施了基于此的中断策略...... phew.另一个例子可能是您是中断策略,而不是while(true)可能已经实现while(!Thread.currentThread().isInterrupted()(这也将使您的代码更加......社交考虑).

所以,总而言之,Condition当你想使用a时,使用的是rougly等同于使用wait/notify/notifyAll Lock,日志是邪恶的,吞咽InterruptedException是顽皮的;)

  • 使用`Condition` +`Lock`****不等同于'Object`同步方法+`synchronized`.前者在等待条件之前允许通知 - 另一方面,如果在`Object.wait()`之前调用`Object.notify()`,则线程将永远阻塞.此外,必须在循环中调用`await()`,请参阅docs. (2认同)

bor*_*jab 17

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

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

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

    public void reportDetonatingEvent(){
        countDownLatch.countDown();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • CountDownLatch 的缺点是它不可重复使用:一旦计数变为零,它就不再可用 (4认同)

Jér*_*nge 6

你可以使用信号量.

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

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

您也可以尝试使用a SynchronousQueue或a CountDownLatch.


axb*_*unt 5

无锁解决方案(?)

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

问题:我最多有一个线程从队列中消耗。多个生产者线程不断插入队列,如果它正在等待,需要通知消费者。队列是无锁的,因此使用锁进行通知会导致生产者线程中出现不必要的阻塞。每个生产者线程都需要先获取锁,然后才能通知等待的消费者。我相信我想出了一个使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

SignalBarrier barrier = new SignalBarrier();
Run Code Online (Sandbox Code Playgroud)

消费者线程(单数,不是复数!):

try {
    while(!conditionIsTrue()) {
        barrier.await();
    }
    doSomethingThatRequiresConditionToBeTrue();
} catch (InterruptedException e) {
    handleInterruption();
}
Run Code Online (Sandbox Code Playgroud)

生产者线程:

doSomethingThatMakesConditionTrue();
barrier.signal();
Run Code Online (Sandbox Code Playgroud)


rde*_*pes 5

人们还可以利用CompletableFutures(从 Java 8 开始):

final CompletableFuture<String> question = new CompletableFuture<>();

// from within the consumer thread:
final String answer = question.get(); // or: event.get(7500000, TimeUnit.YEARS)

// from within the producer thread:
question.complete("42");
Run Code Online (Sandbox Code Playgroud)