Gibt es in Java die Funktion "Blockieren, bis die Bedingung erfüllt ist"?


75

Ich schreibe einen Listener-Thread für einen Server und verwende im Moment:

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

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

Mit dem obigen Code stoße ich auf Probleme mit der Ausführungsfunktion, die die gesamte CPU-Zeitschleife verschlingt. Die Schlaffunktion funktioniert, aber es scheint eine provisorische Lösung zu sein, keine Lösung.

Gibt es eine Funktion, die blockieren würde, bis die Variable 'Bedingung' 'wahr' wird? Oder ist die kontinuierliche Schleife die Standardmethode zum Warten, bis sich der Wert einer Variablen ändert?


3
Warum sollte der obige Code Ihre gesamte CPU verschlingen? Es scheint, dass er nur einmal pro Sekunde gestartet wird. Wie auch immer, sehen Sie diesen Thread: stackoverflow.com/questions/289434/…
jontro

3
Eine vollständige Beschreibung dieses Themas finden Sie in Kapitel 14 der Java-Parallelität in der Praxis . Aber generell, möchten Sie wahrscheinlich auf höherer Ebene Dienstprogramme verwenden wie BlockingQueue, Semaphoreoder CountDownLatcheher als die Low-Level - Mechanismen.
Brian Goetz

Antworten:


68

Eine solche Umfrage ist definitiv die am wenigsten bevorzugte Lösung.

Ich gehe davon aus, dass Sie einen anderen Thread haben, der etwas unternimmt, um die Bedingung zu erfüllen. Es gibt verschiedene Möglichkeiten, Threads zu synchronisieren. Die einfachste in Ihrem Fall wäre eine Benachrichtigung über ein Objekt:

Haupt-Bedroung:

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.
    }
}

Anderer Thread:

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

syncObjectselbst kann eine einfache sein Object.

Es gibt viele andere Möglichkeiten der Kommunikation zwischen Threads, aber welche verwendet werden soll, hängt davon ab, was genau Sie tun.


1
Gern geschehen! Denken Sie daran, dass es andere Möglichkeiten zum Synchronisieren gibt, z. B. Semaphoren, Blockieren von Warteschlangen usw. Alles hängt davon ab, was Sie tun möchten. Objekte sind großartige Tools für die allgemeine Thread-Synchronisierung. Viel Glück mit deiner App!
EboMike

12
Der Try-Catch sollte in eine Schleife eingewickelt werden, in der die tatsächliche Grundbedingung getestet wird, um ein falsches Aufwachen zu verhindern (siehe Wartedokument).
Lawrence Dol

9
Wenn notifyAll zuerst aufgerufen wird, wartet wait () für immer, obwohl die Bedingung erfüllt war, bevor das Warten begann.
Peter Lawrey

3
Diese Antwort ist ziemlich veraltet, da java.concurent herausgekommen ist. Die sauberere und weniger fehleranfällige Art zu warten ist die Verwendung von CountDownLatch pro effektivem Java Ed 2
Alan Berezin

1
@PeterLawrey Es sollte auch beachtet werden (sogar mehr als acht Jahre nach dieser Antwort), dass die Verwendung von notfifyanstelle von notifyAllzu lustigen Effekten führen kann, wenn ein anderer Thread ebenfalls auf diese Weise wartet, da notifynur einer der wartenden Threads benachrichtigt wird (vorausgesetzt, dass dies der Fall ist) zufällig).
Lothar

52

Die Antwort von EboMike und Toby sind beide auf dem richtigen Weg, aber beide enthalten einen fatalen Fehler. Der Fehler wird als verlorene Benachrichtigung bezeichnet .

Das Problem ist, wenn ein Thread aufruft foo.notify(), wird er überhaupt nichts tun, es sei denn, ein anderer Thread schläft bereits in einem foo.wait()Aufruf. Das Objekt fooerinnert sich nicht daran, dass es benachrichtigt wurde.

Es gibt einen Grund, warum Sie nicht anrufen dürfen foo.wait()oder es foo.notify()sei denn, der Thread ist auf foo synchronisiert. Dies liegt daran, dass der einzige Weg, um eine verlorene Benachrichtigung zu vermeiden, darin besteht, den Zustand mit einem Mutex zu schützen. Wenn es richtig gemacht ist, sieht es so aus:

Verbraucher-Thread:

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

Produzententhread:

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

Der Code, der die Bedingung ändert, und der Code, der die Bedingung überprüft, werden alle für dasselbe Objekt synchronisiert, und der Consumer-Thread testet die Bedingung explizit, bevor er wartet. Es gibt keine Möglichkeit für den Verbraucher, die Benachrichtigung zu verpassen und für immer in einem wait()Anruf zu stecken , wenn die Bedingung bereits erfüllt ist.

Beachten Sie auch, dass sich das wait()in einer Schleife befindet. Dies liegt daran, dass im Allgemeinen, wenn der Verbraucher die fooSperre wiedererlangt und aufwacht, ein anderer Thread die Bedingung möglicherweise erneut falsch gemacht hat. Auch wenn dies in Ihrem Programm nicht möglich ist, können Sie in einigen Betriebssystemen foo.wait()auch dann zurückkehren, wenn dies foo.notify()nicht aufgerufen wurde. Dies wird als falsches Aufwecken bezeichnet und ist zulässig, da es die Implementierung von Wartezeiten / Benachrichtigungen auf bestimmten Betriebssystemen erleichtert.


1
Sollten wir den Try-Catch innerhalb oder außerhalb der while-Schleife platzieren? Welcher Weg wird empfohlen und warum?
Jaydev

1
@ JaydevKalivarapu, Angenommen, Sie fragen nach dem InterruptedException, richtig? Es liegt an Ihnen, zu entscheiden, was ein Interrupt bedeutet, aber in den meisten Fällen bedeutet dies wahrscheinlich "aufhören zu warten" und etwas anderes zu tun (wie zum Beispiel das gesamte Programm herunterzufahren). In den meisten Fällen möchten Sie es also um wie in meinem obigen Beispiel auszusehen, mit dem Interrupt-Handler außerhalb der Schleife.
Solomon Slow

6
@ JaydevKalivarapu, PS: Als ich die obige Antwort schrieb, war mir nicht bewusst, dass dieses Muster einen Namen hat: Die Oracle Java-Tutorials nennen es einen geschützten Block . Sie können darüber unter docs.oracle.com/javase/tutorial/essential/concurrency/…
Solomon Slow

1
@ JianGuo, foo.wait()wird werfen, IllegalMonitorStateExceptionwenn foonicht gesperrt ist. Das soll Sie daran erinnern, dass es wait()in Code, der die Sperre nicht hält, keinen Sinn macht. Meine Antwort oben berührt den Grund dafür, aber wenn Sie eine gründliche Erklärung wünschen, sollten Sie das Tutorial lesen. docs.oracle.com/javase/tutorial/essential/concurrency/…
Solomon Slow

1
@ JianGuo, wenn du das tust, dann könnte was passieren; (1) Der Verbraucher testet die Bedingung und stellt fest, dass sie falsch ist. (2) Der Verbraucher versucht einzugeben synchronized(foo), sie wird jedoch blockiert, da der Produzent bereits synchronisiert fooist. (3) Der Produzent bewirkt, dass die Bedingung wahr wird, ruft auf foo.notify()und Dann wird die Sperre aufgehoben. (4) Der Verbraucher betritt den synchronized(foo)Block und ruft an foo.wait(). Jetzt wartet der Verbraucher nicht mehr auf eine Benachrichtigung, die niemals eintrifft. Dieses Problem wird manchmal als "Benachrichtigung verloren" bezeichnet.
Solomon Slow

25

Da hat niemand eine Lösung mit CountDownLatch veröffentlicht. Wie wäre es mit:

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

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

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

21

Ähnlich wie bei der Antwort von EboMike können Sie einen ähnlichen Mechanismus wie wait / notify / notifyAll verwenden, der jedoch auf die Verwendung von a ausgerichtet ist Lock.

Zum Beispiel,

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

Wenn Sie auf eine Bedingung warten, die von einem anderen Thread benachrichtigt wird (in diesem Fall wird aufgerufen doSomethingElse), wird an diesem Punkt der erste Thread fortgesetzt ...

Die Verwendung von Locks gegenüber der intrinsischen Synchronisation hat viele Vorteile, aber ich bevorzuge nur ein explizites ConditionObjekt, um die Bedingung darzustellen (Sie können mehr als eines haben, was für Dinge wie Produzent-Konsument eine nette Geste ist).

Ich kann auch nicht anders, als zu bemerken, wie Sie mit der unterbrochenen Ausnahme in Ihrem Beispiel umgehen. Sie sollten die Ausnahme wahrscheinlich nicht wie folgt verwenden, sondern stattdessen das Interrupt-Status-Flag mit zurücksetzen Thread.currentThread().interrupt.

Dies liegt daran, dass, wenn die Ausnahme ausgelöst wird, das Interrupt-Status-Flag zurückgesetzt wurde (es heißt " Ich erinnere mich nicht mehr daran, unterbrochen worden zu sein, ich kann niemand anderem sagen, dass ich es war, wenn er danach fragt ") und ein anderer Prozess möglicherweise Verlassen Sie sich auf diese Frage. Das Beispiel ist, dass etwas anderes eine Unterbrechungsrichtlinie implementiert hat, die auf diesem ... Puh basiert. Ein weiteres Beispiel könnte sein, dass Ihre Unterbrechungsrichtlinie eher while(true)implementiert wurde als while(!Thread.currentThread().isInterrupted()(was auch dazu führt, dass Ihr Code ... sozial rücksichtsvoller wird).

Zusammenfassend ist die Verwendung Conditionalso ungefähr gleichbedeutend mit der Verwendung von wait / notify / notifyAll, wenn Sie a verwenden möchten Lock. Die Protokollierung ist böse und das Schlucken InterruptedExceptionist ungezogen;)


2
Mit Condition+ Lockist nicht äquivalent zu den ObjectSynchronisationsmethoden + synchronized. Ersteres erlaubt Benachrichtigungen, bevor die Bedingung erwartet wird. Wenn Sie jedoch Object.notify()zuvor aufrufen Object.wait(), wird der Thread für immer blockiert. Außerdem await()muss in einer Schleife aufgerufen werden, siehe docs.
TheOperator

@TheOperator In Bezug auf "Die ersteren erlauben Benachrichtigungen, bevor die Bedingung erwartet wird" - Ich habe im Javadoc nach ConditionText gesucht, der diese Behauptung stützt , konnte ihn jedoch nicht finden. Können Sie bitte Ihre Aussage erklären?
Nayuki

1
Der Beispielcode ist falsch, wait muss in einer Schleife aufgerufen werden. Informationen zum Zustand finden Sie im API-Dokument.
Nathan Hughes

7

Sie könnten ein Semaphor verwenden .

Während die Bedingung nicht erfüllt ist, erfasst ein anderer Thread das Semaphor.
Ihr Thread würde versuchen, es mit acquireUninterruptibly()
oder zu erwerben tryAcquire(int permits, long timeout, TimeUnit unit)und würde blockiert werden.

Wenn die Bedingung erfüllt ist, wird auch das Semaphor freigegeben und Ihr Thread würde es erwerben.

Sie können auch versuchen, ein SynchronousQueueoder ein zu verwenden CountDownLatch.


4

Schlossfreie Lösung (?)

Ich hatte das gleiche Problem, wollte aber eine Lösung, bei der keine Sperren verwendet wurden.

Problem: Ich habe höchstens einen Thread aus einer Warteschlange. Mehrere Producer-Threads werden ständig in die Warteschlange eingefügt und müssen den Consumer benachrichtigen, wenn er wartet. Die Warteschlange ist sperrenfrei, sodass die Verwendung von Sperren für Benachrichtigungen zu unnötigen Blockierungen in Producer-Threads führt. Jeder Producer-Thread muss die Sperre erwerben, bevor er den wartenden Consumer benachrichtigen kann. Ich glaube , ich kam mit einer Lock-freier Lösung mit LockSupportund AtomicReferenceFieldUpdater. Wenn im JDK eine sperrfreie Barriere vorhanden ist, konnte ich sie nicht finden. Beide CyclicBarrierund CoundDownLatchverwenden intern Sperren von dem, was ich finden konnte.

Dies ist mein leicht abgekürzter Code. Mit diesem Code kann nur ein Thread gleichzeitig warten. Es könnte geändert werden, um mehrere Kellner / Verbraucher zuzulassen, indem eine Art atomare Sammlung zum Speichern mehrerer Eigentümer verwendet wird (a ConcurrentMapfunktioniert möglicherweise).

Ich habe diesen Code verwendet und es scheint zu funktionieren. Ich habe es nicht ausgiebig getestet. Ich empfehle Ihnen, die Dokumentation LockSupportvor der Verwendung zu lesen .

/* 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);
    }
}

Um ein vages Beispiel für die Verwendung zu geben, werde ich das Beispiel von James Large übernehmen:

SignalBarrier barrier = new SignalBarrier();

Verbraucher-Thread (Singular, nicht Plural! ):

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

Produzenten-Thread (s):

doSomethingThatMakesConditionTrue();
barrier.signal();
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.