existe-t-il une fonction "bloquer jusqu'à ce que la condition devienne vraie" en java?


J'écris un thread d'écoute pour un serveur, et en ce moment j'utilise:

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

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

Avec le code ci-dessus, je rencontre des problèmes avec la fonction run qui mange tout le temps CPU en boucle. La fonction de sommeil fonctionne, mais il semble être une solution de fortune, pas une solution.

Existe-t-il une fonction qui bloquerait jusqu'à ce que la variable 'condition' devienne 'true'? Ou le bouclage continu est-il la méthode standard d'attente jusqu'à ce que la valeur d'une variable change?

Author: Alex Weitz, 2011-05-14

6 answers

L'interrogation comme celle-ci est certainement la solution la moins préférée.

Je suppose que vous avez un autre thread qui fera quelque chose pour rendre la condition vraie. Il existe plusieurs façons de synchroniser les threads. Le plus simple dans votre cas serait une notification via un objet:

Fil principal:

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

Autre fil:

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

syncObject lui-même peut être un simple Object.

Il existe de nombreuses autres façons de communiquer entre threads, mais laquelle utiliser dépend de quoi précisément, vous êtes en train de faire.

 69
Author: EboMike, 2018-06-23 16:47:11

La réponse d'EboMike et la réponse de Toby sont toutes deux sur la bonne voie, mais elles contiennent toutes deux un défaut fatal. La faille s'appelle notification perdue .

Le problème est que si un thread appelle foo.notify(), il ne fera rien du tout sauf si un autre thread dort déjà dans un appel foo.wait(). L'objet, foo, ne se souvient pas qu'il a été notifiée.

Il y a une raison pour laquelle vous n'êtes pas autorisé à appeler foo.wait() ou foo.notify() sauf si le thread est synchronisé sur foo. C'est parce que la seule façon d'éviter la notification perdue est de protéger la condition avec un mutex. Quand c'est bien fait, cela ressemble à ceci:

Fil consommateur:

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

Fil du producteur:

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

Le code qui modifie la condition et le code qui vérifie la condition sont tous synchronisés sur le même objet, et le thread consommateur teste explicitement la condition avant d'attendre. Il n'y a aucun moyen pour le consommateur de manquer la notification et se retrouver coincé pour toujours dans un appel wait() lorsque la condition est déjà vraie.

Notez également que le wait() est en boucle. En effet, dans le cas général, au moment où le consommateur ré-acquiert le verrou foo et se réveille, un autre thread pourrait avoir rendu la condition fausse à nouveau. Même si ce n'est pas possible dans votre programme, ce qui est possible, dans certains systèmes d'exploitation, est de foo.wait() pour revenir, même lorsque foo.notify() n'a pas été appelé. C'est ce qu'on appelle un réveil fallacieux , et il est permis de se produire parce que cela rend wait/notify plus facile à implémenter sur certains systèmes d'exploitation.

 53
Author: Solomon Slow, 2018-06-23 17:03:09

Comme personne n'a publié une solution avec CountDownLatch. Qu'en est-il:

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

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

    public void reportDetonatingEvent(){
        countDownLatch.countDown();
    }
}
 26
Author: borjab, 2018-06-23 17:01:58

Similaire à la réponse d'EboMike, vous pouvez utiliser un mécanisme similaire à wait/notify/notifyAll mais adapté pour utiliser un Lock.

Par exemple

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

Où vous attendrez une condition qui est notifiée par un autre thread (dans ce cas en appelant doSomethingElse), à ce moment-là, le premier thread continuera...

Utiliser Lock s sur la synchronisation intrinsèque a beaucoup d'avantages mais je préfère simplement avoir un objet Condition explicite pour représenter la condition (vous pouvez avoir plus qu'un qui est une belle touche pour des choses comme producteur-consommateur).

De plus, je ne peux m'empêcher de remarquer comment vous traitez l'exception interrompue dans votre exemple. Vous ne devriez probablement pas consommer l'exception comme celle-ci, mais réinitialiser l'indicateur d'état d'interruption en utilisant Thread.currentThread().interrupt.

Ceci parce que si l'exception est levée, l'indicateur d'état d'interruption aura été réinitialisé (il dit " Je ne me souviens plus d'avoir été interrompu, je ne pourrai dire à personne d'autre que j'ai si ils demandent") et un autre processus peut s'appuyer sur cette question. L'exemple étant que quelque chose d'autre a implémenté une stratégie d'interruption basée sur cela... ouf. Un autre exemple pourrait être que votre stratégie d'interruption, plutôt que while(true) aurait pu être implémenté en tant que while(!Thread.currentThread().isInterrupted() (ce qui rendra également votre code plus... socialement prévenant).

Donc, en résumé, utiliser {[4] } équivaut à utiliser wait/notify / notifyAll lorsque vous souhaitez utiliser un Lock, la journalisation est mauvaise et avaler InterruptedException est méchant;)

 22
Author: Toby, 2020-04-21 09:23:21

Vous pouvez utiliser un sémaphore.

Alors que la condition n'est pas remplie, un autre thread acquiert le sémaphore.
Votre thread essayer de l'acquérir avec acquireUninterruptibly()
ou tryAcquire(int permits, long timeout, TimeUnit unit) et serait bloqué.

Lorsque la condition est remplie, le sémaphore est également libéré et votre thread l'acquiert.

Vous pouvez également essayer d'utiliser un SynchronousQueue ou un CountDownLatch.

 7
Author: Jérôme Verstrynge, 2018-06-23 17:02:13

Solution sans verrouillage(?)

J'ai eu le même problème, mais je voulais une solution qui n'utilisait pas de verrous.

Problème: J'ai au plus un thread consommant dans une file d'attente. Plusieurs threads de production s'insèrent constamment dans la file d'attente et doivent informer le consommateur s'il attend. La file d'attente est sans verrouillage, donc l'utilisation de verrous pour la notification provoque un blocage inutile dans les threads du producteur. Chaque thread producteur doit acquérir le verrou avant de pouvoir informer le consommateur en attente. Je croyez que je suis venu avec une serrure sans solution à l'aide de LockSupport et AtomicReferenceFieldUpdater. Si une barrière sans verrouillage existe dans le JDK, je ne pouvais pas la trouver. Les deux CyclicBarrier et CoundDownLatch utilisent des verrous en interne à partir de ce que j'ai pu trouver.

Ceci est mon code légèrement abrégé. Juste pour être clair, ce code permettra seulement un fil attendre à une époque. Il pourrait être modifié pour permettre plusieurs utilisateurs / consommateurs en utilisant un certain type de collection atomique pour stocker plusieurs propriétaires (a ConcurrentMap peut fonctionner).

J'ai utilisé ce code et cela semble fonctionner. Je ne l'ai pas testé de manière approfondie. Je vous suggère de lire la documentation pour LockSupport avant utilisation.

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

Pour donner un vague exemple d'utilisation, je vais adopter l'exemple de james large:

SignalBarrier barrier = new SignalBarrier();

Consumer thread (singulier, pas pluriel!):

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

Fil (s) producteur (s):

doSomethingThatMakesConditionTrue();
barrier.signal();
 5
Author: axblount, 2015-03-17 15:49:38