web-dev-qa-db-fra.com

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

J'écris un fil d'écoute pour un serveur, et pour le 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 consomme tout le temps de la CPU. La fonction de sommeil fonctionne, mais elle semble être une solution de fortune, pas une solution.

Existe-t-il une fonction qui bloquerait jusqu'à ce que la variable "condition" devienne "vraie"? Ou la boucle continue est-elle la méthode standard consistant à attendre que la valeur d'une variable change?

65
Rolan

Polling comme ceci est certainement la solution la moins préférée.

Je suppose que vous avez un autre fil qui va faire 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:

Filetage 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 nombreux autres moyens de communication inter-thread, mais lequel utiliser dépend de ce que vous faites précisément.

57
EboMike

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

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

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 le seul moyen d'éviter de perdre une notification est de protéger la condition avec un mutex. Quand c'est bien fait, ça ressemble à ça:

Fil de consommation:

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 son attente. Le consommateur n'a aucun moyen de rater la notification et de rester bloqué pour toujours dans un appel wait() lorsque la condition est déjà vraie.

Notez également que la wait() est en boucle. En effet, dans le cas général, au moment où le consommateur ré-acquiert le verrou foo et s’active, un autre thread aurait peut-être rendu la condition fausse à nouveau. Même si cela n’est pas possible dans votre programme , dans certains systèmes d’exploitation, ce qui est possible, c’est que foo.wait() revienne même lorsque foo.notify() n'a pas été appelé. Cela s'appelle un réveil parasite , et cela est autorisé car cela facilite la mise en œuvre de l'attente/notification sur certains systèmes d'exploitation.

42
Solomon Slow

De manière similaire à la réponse d'EboMike, vous pouvez utiliser un mécanisme similaire à wait/notify/notifyAll mais conçu 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();
    }
}

Si vous attendez une condition qui est notifiée par un autre thread (dans ce cas, appelez doSomethingElse), le premier thread continuera à ce moment-là ...

Utiliser Locks par rapport à la synchronisation intrinsèque présente de nombreux avantages, mais je préfère simplement avoir un objet explicite Condition pour représenter la condition (vous pouvez en avoir plusieurs, ce qui est une bonne idée, par exemple producteur-consommateur. ).

De plus, je ne peux pas m'empêcher de remarquer comment vous traitez avec l'exception interrompue dans votre exemple. Vous ne devriez probablement pas utiliser l'exception de cette façon, mais réinitialisez l'indicateur d'état d'interruption à l'aide de Thread.currentThrad().interrupt.

Ceci parce que si l'exception est levée, l'indicateur d'état d'interruption aura été réinitialisé (il est dit "je ne me souviens plus d'avoir été interrompu, je ne pourrai dire à personne d'autre que je l'ai été s'il le demande" ) et un autre processus peut reposer sur cette question. L'exemple étant que quelque chose d'autre a mis en place une politique d'interruption basée sur ceci ... ouf. Un autre exemple pourrait être votre politique d'interruption, plutôt que while(true) aurait été implémenté sous la forme while(!Thread.currentThread().isInterrupted() (ce qui rendra également votre code plus respectueux de la société).

Donc, en résumé, utiliser Condition équivaut grossièrement à attendre/notify/notifyAll quand vous voulez utiliser un Lock, la journalisation est mauvaise et avaler InterruptedException est vilain;)

18
Toby

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

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

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

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

Vous pouvez utiliser un sémaphore .

Tant que la condition n'est pas remplie, un autre thread acquiert le sémaphore.
Votre thread essaierait 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 SynchronousQueue ou CountDownLatch .

5

Solution sans verrouillage (?)

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

Problème: au plus un thread utilise une file d'attente. Plusieurs threads producteurs insèrent constamment dans la file d'attente et doivent informer le consommateur s'il attend. La file d'attente ne comporte pas de verrou. L'utilisation de verrous pour la notification entraîne un blocage inutile des threads producteurs. Chaque thread de producteur doit acquérir le verrou avant de pouvoir avertir le consommateur en attente. Je pense avoir mis au point une solution sans verrouillage utilisant LockSupport et AtomicReferenceFieldUpdater . Si une barrière sans verrou existe dans le JDK, je ne la trouvais pas. CyclicBarrier et CoundDownLatch utilisent tous deux des verrous internes à ce que j'ai pu trouver.

Ceci est mon code légèrement abrégé. Juste pour être clair, ce code ne permettra que un thread à attendre à la fois. Il pourrait être modifié pour permettre à plusieurs demandeurs/utilisateurs d'attendre en utilisant un certain type de collection atomique pour stocker plusieurs propriétaires (un ConcurrentMap peut fonctionner).

J'ai utilisé ce code et il semble fonctionner. Je ne l'ai pas testé de manière approfondie. Je vous suggère de lire la documentation de 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 exemple d'utilisation vague, je vais adopter l'exemple de James Large:

SignalBarrier barrier = new SignalBarrier();

Fil de consommation (singulier, pas au pluriel !):

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

Fils du producteur:

doSomethingThatMakesConditionTrue();
barrier.signal();
4
axblount