web-dev-qa-db-ja.com

javaには「条件が満たされるまでブロックする」機能がありますか?

私はサーバーのリスナースレッドを作成していますが、現在使用しているのは次のとおりです。

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時間のループをすべて消費するrun関数の問題に直面しています。スリープ機能は動作しますが、解決策ではなく、その場しのぎの修正のようです。

変数 'c​​ondition'が 'true'になるまでブロックする関数はありますか?または、変数の値が変化するまで待機する標準的な方法を継続的にループしていますか?

65
Rolan

このようなポーリングは間違いなく最も推奨されない解決策です。

条件を真にするために何かを行う別のスレッドがあると仮定します。スレッドを同期するにはいくつかの方法があります。あなたの場合の最も簡単な方法は、オブジェクトを介した通知です。

メインスレッド:

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にすることができます。

スレッド間通信には他にも多くの方法がありますが、どの方法を使用するかは、正確に何をしているかによって異なります。

57
EboMike

EboMikeの答えTobyの答え は両方とも正しい軌道に乗っていますが、どちらも致命的な欠陥を含んでいます。この欠陥はlost notificationと呼ばれます。

問題は、スレッドがfoo.notify()を呼び出す場合、他のスレッドがfoo.wait()呼び出しで既にスリープしていない限り、何もしません。オブジェクトfooは、通知されたことを覚えていません。

スレッドがfooで同期されない限り、foo.wait()またはfoo.notify()の呼び出しが許可されない理由があります。これは、通知の紛失を回避する唯一の方法がミューテックスで状態を保護することだからです。正しく完了すると、次のようになります。

消費者スレッド:

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

プロデューサースレッド:

_synchronized(foo) {
    doSomethingThatMakesConditionTrue();
    foo.notify();
}
_

条件を変更するコードと条件をチェックするコードはすべて同じオブジェクトで同期され、コンシューマスレッドは待機する前に条件を明示的にテストします。消費者が通知を逃して、条件がすでに真であるときにwait()呼び出しで永久にスタックする方法はありません。

wait()がループしていることにも注意してください。これは、一般的な場合、コンシューマがfooロックを再取得して起動するまでに、他のスレッドが条件を再び偽にした可能性があるためです。 yourプログラムでそれが不可能な場合でも、一部のオペレーティングシステムでは、foo.wait()foo.notify()は呼び出されていません。これはスプリアスウェイクアップと呼ばれ、特定のオペレーティングシステムでの待機/通知の実装が容易になるため、発生が許可されます。

42
Solomon Slow

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を呼び出す)を待つ場所で、その時点で最初のスレッドが続行されます...

固有の同期よりもLocksを使用することには多くの利点がありますが、明示的にConditionオブジェクトを使用して条件を表すことをお勧めします(プロデューサーと消費者のようなものに適したものを複数持つことができます)。

また、私はあなたの例で中断された例外をどのように扱うかに気づかずにはいられません。おそらくこのような例外を消費すべきではなく、代わりにThread.currentThrad().interruptを使用して割り込みステータスフラグをリセットする必要があります。

これは、例外がスローされた場合、割り込みステータスフラグがリセットされるためです(「割り込みが発生したことを覚えていません。他の人に尋ねた場合、行ったことを伝えることができなくなります )および別のプロセスがこの質問に依存する場合があります。他の何かがこれに基づいて中断ポリシーを実装しているという例です...ふう。さらなる例としては、while(true)while(!Thread.currentThread().isInterrupted()として実装されているのではなく、中断ポリシーである可能性があります(これにより、コードがより...社会的に配慮されます)。

したがって、要約すると、Conditionを使用することは、Lockを使用するときにwait/notify/notifyAllを使用することとほぼ同等です。ロギングは悪で、InterruptedExceptionを飲み込むことはいたずらです;)

18
Toby

誰もCountDownLatchを使用したソリューションを公開していないため。どうですか:

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

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

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

セマフォ を使用できます。

条件が満たされていない間、別のスレッドがセマフォを取得します。
あなたのスレッドはacquireUninterruptibly()で取得しようとします
またはtryAcquire(int permits, long timeout, TimeUnit unit)でブロックされます。

条件が満たされると、セマフォも解放され、スレッドがそれを取得します。

SynchronousQueue または CountDownLatch を使用して試すこともできます。

5

ロックフリーソリューション(?)

私は同じ問題を抱えていましたが、ロックを使用しないソリューションが必要でした。

問題:キューから消費するスレッドは最大1つです。複数のプロデューサースレッドが絶えずキューに挿入されており、コンシューマーが待機している場合は通知する必要があります。キューはロックされていないため、通知にロックを使用すると、プロデューサースレッドで不要なブロッキングが発生します。各プロデューサスレッドは、待機中のコンシューマに通知する前にロックを取得する必要があります。 LockSupport および AtomicReferenceFieldUpdater を使用してロックフリーソリューションを思いついたと思います。 JDK内にロックフリーバリアが存在する場合、それを見つけることができませんでした。 CyclicBarrierCoundDownLatchはどちらも、私が見つけたものから内部的にロックを使用します。

これは私の少し短縮されたコードです。明確にするために、このコードでは、oneスレッドのみが一度に待機できるようにします。ある種のアトミックコレクションを使用して複数の所有者を格納することにより、複数の待機者/消費者を許可するように変更できます(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();
4
axblount