Java: notify () vs notifyAll () encore une fois


377

Si on google pour "différence entre notify()et notifyAll()", alors beaucoup d'explications apparaîtront (en laissant de côté les paragraphes javadoc). Tout se résume au nombre de threads en attente en cours de réveil: un dans notify()tous notifyAll().

Cependant (si je comprends bien la différence entre ces méthodes), un seul thread est toujours sélectionné pour une nouvelle acquisition du moniteur; dans le premier cas celui sélectionné par la VM, dans le second cas celui sélectionné par le planificateur de threads système. Les procédures de sélection exactes pour les deux (dans le cas général) ne sont pas connues du programmeur.

Quelle est alors la différence utile entre notify () et notifyAll () ? Suis-je en train de manquer quelque chose?


6
Les bibliothèques utiles à utiliser pour la concurrence sont dans les bibliothèques de concurrence. Je suggère que ce sont un meilleur choix dans presque tous les cas. La bibliothèque Concurency antérieure à Java 5.0 (dans laquelle ils ont été ajoutés en standard en 2004)
Peter Lawrey

4
Je ne suis pas d'accord avec Peter. La bibliothèque de concurrence est implémentée en Java, et il y a beaucoup de code Java exécuté à chaque fois que vous appelez lock (), unlock (), etc. Vous pouvez vous tirer une balle dans le pied en utilisant la bibliothèque de concurrence au lieu de la bonne vieille synchronized, sauf pour certains , cas d'utilisation assez rares.
Alexander Ryzhov

2
Le principal malentendu semble être le suivant: ... un seul thread est toujours sélectionné pour une nouvelle acquisition du moniteur; dans le premier cas celui sélectionné par la VM, dans le second cas celui sélectionné par le planificateur de threads système. L'implication étant que ce sont essentiellement les mêmes. Bien que le comportement décrit soit correct, ce qui manque, c'est que dans le notifyAll()cas, les autres threads après le premier restent éveillés et acquièrent le moniteur un par un. Dans le notifycas, aucun des autres threads n'est même réveillé. Donc, fonctionnellement, ils sont très différents!
BeeOnRope

1) Si plusieurs threads attendent sur un objet, et notify () n'est appelé qu'une seule fois sur cet objet. Excepté l'un des threads en attente, les threads restants attendent pour toujours? 2) Si notify () est utilisé, seul l'un des nombreux threads en attente commence à s'exécuter. Si notifyall () est utilisé, tous les threads en attente sont notifiés mais un seul d'entre eux commence à s'exécuter, alors quelle est l'utilité de notifyall () ici?
Chetan Gowda

@ChetanGowda Notifier tous les threads vs Notifier exactement un seul thread arbitraire a réellement une différence significative jusqu'à ce que cette différence apparemment subtile mais importante nous frappe.Lorsque vous notifiez () seulement 1 thread, tous les autres threads seront en attente jusqu'à ce qu'ils reçoivent une notification explicite /signal. Notifiant tout, tous les threads seront exécutés et complétés dans un ordre l'un après l'autre sans autre notification - ici, nous devons dire que les threads le sont blockedet non waiting.Lorsque blockedson exécution est temporairement suspendue jusqu'à ce qu'un autre thread soit à l'intérieur du syncbloc.
user104309

Réponses:


248

Cependant (si je comprends bien la différence entre ces méthodes), un seul thread est toujours sélectionné pour une acquisition ultérieure du moniteur.

Ce n'est pas correct. o.notifyAll()réveille tous les threads bloqués dans les o.wait()appels. Les threads ne sont autorisés à revenir que l' o.wait()un après l'autre, mais ils auront chacun leur tour.


Autrement dit, cela dépend de la raison pour laquelle vos discussions attendent d'être notifiées. Voulez-vous dire à l'un des fils d'attente que quelque chose s'est produit, ou voulez-vous tous les dire en même temps?

Dans certains cas, tous les threads en attente peuvent prendre des mesures utiles une fois l'attente terminée. Un exemple serait un ensemble de threads attendant qu'une certaine tâche se termine; une fois la tâche terminée, tous les threads en attente peuvent poursuivre leurs activités. Dans un tel cas, vous utiliseriez notifyAll () pour réveiller tous les threads en attente en même temps.

Dans un autre cas, par exemple le verrouillage mutuellement exclusif, un seul des threads en attente peut faire quelque chose d'utile après avoir été notifié (dans ce cas, acquérez le verrou). Dans un tel cas, vous préférez utiliser notify () . Correctement implémenté, vous pouvez également utiliser notifyAll () dans cette situation, mais vous réveilleriez inutilement les threads qui ne peuvent rien faire de toute façon.


Dans de nombreux cas, le code d'attente d'une condition sera écrit en boucle:

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

De cette façon, si un o.notifyAll()appel réveille plusieurs threads en attente et que le premier à revenir des o.wait()marques laisse la condition à l'état faux, les autres threads qui ont été réveillés retourneront en attente.


29
si vous notifiez un seul thread mais plusieurs attendent sur un objet, comment la VM détermine-t-elle celui à notifier?
amphibient

6
Je ne peux pas dire avec certitude sur la spécification Java, mais en général, vous devez éviter de faire des hypothèses sur ces détails. Je pense que vous pouvez supposer que la machine virtuelle le fera d'une manière saine et surtout équitable.
Liedman

15
Liedman a gravement tort, la spécification Java indique explicitement que le notify () n'est pas garanti d'être juste. c'est-à-dire que chaque appel à notifier peut réveiller à nouveau le même thread (la file d'attente de threads dans le moniteur n'est PAS FAIR ou FIFO). Cependant, l'ordonnanceur est garanti d'être juste. C'est pourquoi dans la plupart des cas où vous avez plus de 2 threads, vous devriez préférer notifyAll.
Yann TM

45
@YannTM Je suis tout à fait favorable à une critique constructive, mais je pense que votre ton est un peu injuste. J'ai dit explicitement "je ne peux pas dire avec certitude" et "je pense". Tranquillement, avez-vous déjà écrit quelque chose il y a sept ans qui n'était pas correct à 100%?
Liedman

10
Le problème est que c'est la réponse acceptée, ce n'est pas une question de fierté personnelle. Si vous savez que vous vous êtes trompé maintenant, veuillez modifier votre réponse pour le dire, et indiquer par exemple xagyg pédagogique et la bonne réponse ci-dessous.
Yann TM

330

Clairement, notifyréveille (n'importe quel) un thread de l'ensemble d'attente, notifyAllréveille tous les threads de l'ensemble d'attente. La discussion suivante devrait dissiper tous les doutes. notifyAlldoit être utilisé la plupart du temps. Si vous ne savez pas lequel utiliser, utilisez notifyAll.Veuillez voir l'explication qui suit.

Lisez très attentivement et comprenez. Veuillez m'envoyer un e-mail si vous avez des questions.

Regardez producteur / consommateur (l'hypothèse est une classe ProducerConsumer avec deux méthodes). IL EST CASSÉ (car il utilise notify) - oui cela PEUT fonctionner - même la plupart du temps, mais cela peut aussi provoquer un blocage - nous verrons pourquoi:

public synchronized void put(Object o) {
    while (buf.size()==MAX_SIZE) {
        wait(); // called if the buffer is full (try/catch removed for brevity)
    }
    buf.add(o);
    notify(); // called in case there are any getters or putters waiting
}

public synchronized Object get() {
    // Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
    while (buf.size()==0) {
        wait(); // called if the buffer is empty (try/catch removed for brevity)
        // X: this is where C1 tries to re-acquire the lock (see below)
    }
    Object o = buf.remove(0);
    notify(); // called if there are any getters or putters waiting
    return o;
}

TOUT D'ABORD,

Pourquoi avons-nous besoin d'une boucle while entourant l'attente?

Nous avons besoin d'une whileboucle au cas où nous aurions cette situation:

Le consommateur 1 (C1) entre dans le bloc synchronisé et le tampon est vide, donc C1 est mis dans le jeu d'attente (via l' waitappel). Le consommateur 2 (C2) est sur le point d'entrer dans la méthode synchronisée (au point Y ci-dessus), mais le producteur P1 place un objet dans le tampon, puis appelle notify. Le seul thread en attente est C1, il est donc réveillé et tente maintenant de ré-acquérir le verrou d'objet au point X (ci-dessus).

C1 et C2 tentent maintenant d'acquérir le verrou de synchronisation. L'un d'eux (non déterministe) est choisi et entre dans la méthode, l'autre est bloqué (pas en attente - mais bloqué, essayant d'acquérir le verrou sur la méthode). Disons que C2 obtient le verrou en premier. C1 bloque toujours (essaye d'acquérir le verrou en X). C2 termine la méthode et libère le verrou. Maintenant, C1 acquiert le verrou. Devinez quoi, heureusement que nous avons une whileboucle, car C1 effectue la vérification de boucle (garde) et ne peut pas supprimer un élément inexistant du tampon (C2 l'a déjà!). Si nous n'en avions pas while, nous obtiendrions un IndexArrayOutOfBoundsExceptioncomme C1 essaie de supprimer le premier élément du tampon!

MAINTENANT,

Ok, maintenant pourquoi avons-nous besoin de notifier tout?

Dans l'exemple producteur / consommateur ci-dessus, il semble que nous pouvons nous en sortir notify. Cela semble ainsi, car nous pouvons prouver que les gardes des boucles d' attente pour le producteur et le consommateur s'excluent mutuellement. Autrement dit, il semble que nous ne pouvons pas avoir de thread en attente dans la putméthode ainsi que dans la getméthode, car, pour que cela soit vrai, les éléments suivants doivent être vrais:

buf.size() == 0 AND buf.size() == MAX_SIZE (supposez que MAX_SIZE n'est pas 0)

CEPENDANT, ce n'est pas assez bon, nous devons utiliser notifyAll. Voyons pourquoi ...

Supposons que nous ayons un tampon de taille 1 (pour rendre l'exemple facile à suivre). Les étapes suivantes nous mènent à l'impasse. Notez qu'à TOUT MOMENT un thread est réveillé avec notifier, il peut être sélectionné de manière non déterministe par la JVM - c'est-à-dire que n'importe quel thread en attente peut être réveillé. Notez également que lorsque plusieurs threads bloquent l'entrée d'une méthode (c'est-à-dire en essayant d'acquérir un verrou), l'ordre d'acquisition peut être non déterministe. Souvenez-vous également qu'un thread ne peut être que dans l'une des méthodes à la fois - les méthodes synchronisées permettent à un seul thread d'exécuter (c'est-à-dire de maintenir le verrou de) toutes les méthodes (synchronisées) de la classe. Si la séquence d'événements suivante se produit - des résultats de blocage:

ÉTAPE 1:
- P1 met 1 caractère dans le tampon

ÉTAPE 2:
- P2 tente put- vérifie la boucle d'attente - déjà un caractère - attend

ÉTAPE 3:
- P3 tente put- vérifie la boucle d'attente - déjà un caractère - attend

ÉTAPE 4:
- C1 tente d'obtenir 1 caractère
- C2 tente d'obtenir 1 caractère - blocs à l'entrée de la getméthode
- C3 tente d'obtenir 1 caractère - blocs à l'entrée de la getméthode

ÉTAPE 5:
- C1 exécute la getméthode - obtient la méthode char, calls notify, exit
- Le notifyréveille P2
- MAIS, C2 entre dans la méthode avant que P2 ne puisse (P2 doit réacquérir le verrou), donc P2 bloque à l'entrée dans la putméthode
- C2 vérifie la boucle d'attente, plus de caractères dans le tampon, donc attend
- C3 entre dans la méthode après C2, mais avant P2, vérifie la boucle d'attente, plus de caractères dans le tampon, donc attend

ÉTAPE 6:
- MAINTENANT: il y a P3, C2 et C3 en attente!
- Enfin P2 acquiert le verrou, met un caractère dans le tampon, les appels notifient, quitte la méthode

ÉTAPE 7:
- La notification de P2 réveille P3 (rappelez-vous que n'importe quel thread peut être réveillé)
- P3 vérifie la condition de la boucle d'attente, il y a déjà un caractère dans le tampon, alors attend.
- PLUS DE FILS POUR APPELER NOTIFIER ET TROIS FILS SUSPENDUS PERMANENT!

SOLUTION: Remplacer notifypar notifyAlldans le code producteur / consommateur (ci-dessus).


1
finnw - P3 doit revérifier la condition car le notifyP3 (le thread sélectionné dans cet exemple) se poursuit à partir du point où il était en attente (c'est-à-dire dans la whileboucle). Il existe d'autres exemples qui ne provoquent pas de blocage, cependant, dans ce cas, l'utilisation de notifyne garantit pas un code sans blocage. L'utilisation de notifyAllne.
xagyg

4
@marcus Très proche. Avec notifyAll, chaque thread réacquiert le verrou (un à la fois), mais notez qu'après qu'un thread a réacquis le verrou et exécuté la méthode (puis est sorti) ... le thread suivant réacquiert le verrou, vérifie le "pendant" et reviendra à "attendre" (en fonction de la condition bien sûr). Donc, notifiez wakes one thread - comme vous le dites correctement. notifyAll réveille tous les threads et chaque thread réacquiert le verrou un à la fois - vérifie la condition du "while" et exécute la méthode ou "attend" à nouveau.
xagyg

1
@xagyg, parlez-vous d'un scénario dans lequel chaque producteur n'a qu'un seul caractère à stocker? Si c'est le cas, votre exemple est correct, mais pas très intéressant OMI. Avec les étapes supplémentaires que je suggère, vous pouvez bloquer le même système, mais avec une quantité illimitée d'entrée - c'est ainsi que de tels modèles sont généralement utilisés dans la réalité.
eran

3
@codeObserver Vous avez demandé: "L'appel de notifyAll () conduirait-il à plusieurs threads en attente à vérifier la condition while () en même temps. exception?. " Non, ce n'est pas possible, car bien que plusieurs threads se réveillent, ils ne peuvent pas vérifier la condition while en même temps. Ils doivent chacun atteindre à nouveau le verrou (immédiatement après l'attente) avant de pouvoir entrer à nouveau dans la section de code et revérifier le tout. Par conséquent, un à la fois.
xagyg

4
@xagyg bel exemple. C'est hors sujet de la question d'origine; juste pour le plaisir de la discussion. L'impasse est un problème de conception imo (corrigez-moi si je me trompe). Parce que vous avez un verrou partagé par put et get. Et JVM n'est pas assez intelligent pour appeler put après une libération du verrou et vice versa. Le verrou mort se produit parce que put réveille un autre put, qui se remet à attendre () à cause de while (). Est-ce que faire fonctionner deux classes (et deux serrures)? Donc, mettez {synchonized (get)}, get {(synchonized (put)}. En d'autres termes, get se réveillera uniquement, et put se réveillera uniquement.
Jay

43

Différences utiles:

  • Utilisez notify () si tous vos threads en attente sont interchangeables (l'ordre dans lequel ils se réveillent n'a pas d'importance), ou si vous n'avez qu'un seul thread en attente. Un exemple courant est un pool de threads utilisé pour exécuter des travaux à partir d'une file d'attente - lorsqu'un travail est ajouté, l'un des threads est averti de se réveiller, d'exécuter le travail suivant et de se rendormir.

  • Utilisez notifyAll () pour d'autres cas où les threads en attente peuvent avoir des objectifs différents et devraient pouvoir s'exécuter simultanément. Un exemple est une opération de maintenance sur une ressource partagée, où plusieurs threads attendent la fin de l'opération avant d'accéder à la ressource.


19

Je pense que cela dépend de la façon dont les ressources sont produites et consommées. Si 5 objets de travail sont disponibles à la fois et que vous avez 5 objets de consommation, il serait judicieux de réveiller tous les threads à l'aide de notifyAll () afin que chacun puisse traiter 1 objet de travail.

Si vous n'avez qu'un seul objet de travail disponible, quel est l'intérêt de réveiller tous les objets de consommation pour courir pour cet objet? Le premier vérifiant le travail disponible l'obtiendra et tous les autres threads vérifieront et découvriront qu'ils n'ont rien à faire.

J'ai trouvé une excellente explication ici . En bref:

La méthode notify () est généralement utilisée pour les pools de ressources , où il existe un nombre arbitraire de «consommateurs» ou de «travailleurs» qui prennent des ressources, mais lorsqu'une ressource est ajoutée au pool, un seul des consommateurs ou des travailleurs en attente peut traiter avec ça. La méthode notifyAll () est en fait utilisée dans la plupart des autres cas. Strictement, il est nécessaire d'informer les serveurs d'une condition qui pourrait permettre à plusieurs serveurs de continuer. Mais cela est souvent difficile à savoir. Donc, en règle générale, si vous n'avez pas de logique particulière pour utiliser notify (), alors vous devriez probablement utiliser notifyAll () , car il est souvent difficile de savoir exactement quels threads attendront un objet particulier et pourquoi.


11

Notez qu'avec les utilitaires de simultanéité, vous avez également le choix entre signal()et signalAll()comme ces méthodes y sont appelées. La question reste donc valable même avec java.util.concurrent.

Doug Lea soulève un point intéressant dans son célèbre livre : si un notify()et Thread.interrupt()se produisent en même temps, la notification pourrait en fait se perdre. Si cela peut se produire et a des implications dramatiques, notifyAll()c'est un choix plus sûr même si vous payez le prix des frais généraux (réveiller trop de threads la plupart du temps).


10

Court résumé:

Préférez toujours notifyAll () à notify () sauf si vous avez une application massivement parallèle où un grand nombre de threads font tous la même chose.

Explication:

notify () [...] réveille un seul thread. Étant donné que notify () ne vous permet pas de spécifier le thread qui est réveillé, il n'est utile que dans les applications massivement parallèles - c'est-à-dire les programmes avec un grand nombre de threads, tous effectuant des tâches similaires. Dans une telle application, vous ne vous souciez pas du thread qui se réveille.

source: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

Comparez notify () avec notifyAll () dans la situation décrite ci-dessus: une application massivement parallèle où les threads font la même chose. Si vous appelez notifyAll () dans ce cas, notifyAll () provoquera le réveil (c.-à-d. La planification) d'un grand nombre de threads, beaucoup d'entre eux inutilement (car un seul thread peut réellement continuer, à savoir le thread qui recevra la surveiller l'objet wait () , notify () ou notifyAll () a été appelé), gaspillant ainsi des ressources informatiques.

Ainsi, si vous n'avez pas d'application où un grand nombre de threads font la même chose simultanément, préférez notifyAll () à notify () . Pourquoi? Parce que, comme d'autres utilisateurs ont déjà répondu sur ce forum, notifiez ()

réveille un seul thread qui attend sur le moniteur de cet objet. [...] Le choix est arbitraire et se fait à la discrétion de la mise en œuvre.

source: API Java SE8 ( https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify-- )

Imaginez que vous ayez une application de consommateur producteur où les consommateurs sont prêts (c'est-à-dire en attente ) à consommer, les producteurs sont prêts (c'est-à-dire en attente ) à produire et la file d'attente d'articles (à produire / consommer) est vide. Dans ce cas, notify () pourrait ne réveiller que les consommateurs et jamais les producteurs car le choix de ceux qui se réveillent est arbitraire . Le cycle de consommation des producteurs ne progressera pas, bien que les producteurs et les consommateurs soient respectivement prêts à produire et à consommer. Au lieu de cela, un consommateur est réveillé (c'est-à-dire qu'il quitte le statut wait () ), ne retire pas un élément de la file d'attente car il est vide et avertit () s un autre consommateur de continuer.

En revanche, notifyAll () réveille à la fois les producteurs et les consommateurs. Le choix qui est planifié dépend du planificateur. Bien sûr, en fonction de l'implémentation du planificateur, le planificateur peut également uniquement planifier des consommateurs (par exemple, si vous attribuez une priorité très élevée aux threads de consommateurs). Cependant, l'hypothèse ici est que le danger que le planificateur ne planifie que les consommateurs est inférieur au risque que la JVM ne réveille que les consommateurs car tout planificateur raisonnablement implémenté ne prend pas que des décisions arbitraires . Au contraire, la plupart des implémentations du planificateur font au moins un effort pour éviter la famine.


9

Voici un exemple. Exécuter. Ensuite, changez l'un des notifyAll () pour notifier () et voir ce qui se passe.

Classe ProducerConsumerExample

public class ProducerConsumerExample {

    private static boolean Even = true;
    private static boolean Odd = false;

    public static void main(String[] args) {
        Dropbox dropbox = new Dropbox();
        (new Thread(new Consumer(Even, dropbox))).start();
        (new Thread(new Consumer(Odd, dropbox))).start();
        (new Thread(new Producer(dropbox))).start();
    }
}

Classe Dropbox

public class Dropbox {

    private int number;
    private boolean empty = true;
    private boolean evenNumber = false;

    public synchronized int take(final boolean even) {
        while (empty || evenNumber != even) {
            try {
                System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                wait();
            } catch (InterruptedException e) { }
        }
        System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
        empty = true;
        notifyAll();

        return number;
    }

    public synchronized void put(int number) {
        while (!empty) {
            try {
                System.out.println("Producer is waiting ...");
                wait();
            } catch (InterruptedException e) { }
        }
        this.number = number;
        evenNumber = number % 2 == 0;
        System.out.format("Producer put %d.%n", number);
        empty = false;
        notifyAll();
    }
}

Classe consommateur

import java.util.Random;

public class Consumer implements Runnable {

    private final Dropbox dropbox;
    private final boolean even;

    public Consumer(boolean even, Dropbox dropbox) {
        this.even = even;
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            dropbox.take(even);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) { }
        }
    }
}

Classe de producteur

import java.util.Random;

public class Producer implements Runnable {

    private Dropbox dropbox;

    public Producer(Dropbox dropbox) {
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            int number = random.nextInt(10);
            try {
                Thread.sleep(random.nextInt(100));
                dropbox.put(number);
            } catch (InterruptedException e) { }
        }
    }
}

8

De Joshua Bloch, le gourou de Java lui-même dans Effective Java 2nd edition:

"Article 69: Préférer les utilitaires de simultanéité à attendre et à notifier".


16
Le pourquoi est plus important que la source.
Pacerier

2
@Pacerier Bien dit. Je serais plus intéressé à en découvrir également les raisons. Une raison possible pourrait être que l'attente et la notification dans la classe d'objets sont basées sur une variable de condition implicite. Ainsi, dans l'exemple standard du producteur et du consommateur ..... le producteur et le consommateur attendront dans la même condition, ce qui pourrait conduire à un blocage, comme l'explique xagyg dans sa réponse. Une meilleure approche consiste donc à utiliser 2 variables de condition comme expliqué dans docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
rahul

6

J'espère que cela dissipera certains doutes.

notify () : La méthode notify () réveille un thread en attente du verrou (le premier thread qui a appelé wait () sur ce verrou).

notifyAll () : La méthode notifyAll () réveille tous les threads en attente de verrouillage; la JVM sélectionne l'un des threads dans la liste des threads en attente de verrouillage et le réveille.

Dans le cas d'un seul thread en attente d'un verrou, il n'y a pas de différence significative entre notify () et notifyAll (). Cependant, lorsque plusieurs threads attendent le verrouillage, dans notify () et notifyAll (), le thread exact réveillé est sous le contrôle de la JVM et vous ne pouvez pas contrôler par programmation le réveil d'un thread spécifique.

À première vue, il semble que ce soit une bonne idée d'appeler simplement notify () pour réveiller un thread; il peut sembler inutile de réveiller tous les fils. Cependant, le problème avec notify () est que le thread réveillé peut ne pas être celui qui convient pour être réveillé (le thread peut attendre une autre condition, ou la condition n'est toujours pas satisfaite pour ce thread, etc.). Dans ce cas , la notification () peut être perdue et aucun autre thread ne se réveillera, ce qui pourrait entraîner un type de blocage (la notification est perdue et tous les autres threads attendent une notification - pour toujours).

Pour éviter ce problème , il est toujours préférable d'appeler notifyAll () lorsqu'il y a plus d'un thread en attente d'un verrou (ou plus d'une condition sur laquelle l'attente est effectuée). La méthode notifyAll () réveille tous les threads, elle n'est donc pas très efficace. cependant, cette perte de performances est négligeable dans les applications du monde réel.


6

Il existe trois états pour un thread.

  1. WAIT - Le thread n'utilise aucun cycle CPU
  2. BLOQUÉ - Le thread est bloqué en essayant d'acquérir un moniteur. Il peut toujours utiliser les cycles du processeur
  3. RUNNING - Le thread est en cours d'exécution.

Désormais, lorsqu'une notification () est appelée, la JVM sélectionne un thread et les déplace vers l'état BLOQUÉ et donc vers l'état EN COURS car il n'y a pas de concurrence pour l'objet moniteur.

Lorsqu'un notifyAll () est appelé, la JVM sélectionne tous les threads et les déplace tous vers l'état BLOQUÉ. Tous ces threads obtiendront le verrou de l'objet sur une base prioritaire. Le thread qui est capable d'acquérir le moniteur en premier sera capable de passer d'abord à l'état RUNNING et ainsi de suite.


Juste une explication géniale.
royatirek

5

Je suis très surpris que personne n'ait mentionné le fameux problème de "réveil perdu" (google it).

Fondamentalement:

  1. si vous avez plusieurs threads en attente dans les mêmes conditions et,
  2. plusieurs threads qui peuvent vous faire passer de l'état A à l'état B et,
  3. plusieurs threads qui peuvent vous faire passer de l'état B à l'état A (généralement les mêmes threads qu'en 1.) et,
  4. la transition de l'état A à B doit notifier les threads en 1.

ALORS, vous devez utiliser notifyAll sauf si vous avez des garanties prouvables que les réveils perdus sont impossibles.

Un exemple courant est une file d'attente FIFO simultanée où: plusieurs mises en file d'attente (1. et 3. ci-dessus) peuvent faire passer votre file d'attente de vides à non-vides plusieurs files d'attente (2. ci-dessus) peuvent attendre la condition "la file d'attente n'est pas vide" vide -> non vide doit informer les dequeuers

Vous pouvez facilement écrire un entrelacement d'opérations dans lequel, à partir d'une file d'attente vide, 2 enqueuers et 2 dequeuers interagissent et 1 enqueuer reste en veille.

Il s'agit d'un problème sans doute comparable au problème de blocage.


Mes excuses, xagyg l'explique en détail. Le nom du problème est "réveil perdu"
NickV

@Abhay Bansal: Je pense que vous manquez le fait que condition.wait () libère le verrou et il est récupéré par le thread qui se réveille.
NickV

4

notify()réveillera un thread tandis que notifyAll()réveillera tout. Pour autant que je sache, il n'y a pas de compromis. Mais si vous n'êtes pas sûr de ce que notify()feront vos threads, utilisez notifyAll(). Fonctionne comme un charme à chaque fois.


4

Toutes les réponses ci-dessus sont correctes, pour autant que je sache, donc je vais vous dire autre chose. Pour le code de production, vous devez vraiment utiliser les classes dans java.util.concurrent. Ils ne peuvent pas faire grand-chose pour vous, dans le domaine de la concurrence en Java.


4

notify()vous permet d'écrire du code plus efficace que notifyAll().

Considérez le morceau de code suivant qui est exécuté à partir de plusieurs threads parallèles:

synchronized(this) {
    while(busy) // a loop is necessary here
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notifyAll();
}

Il peut être rendu plus efficace en utilisant notify():

synchronized(this) {
    if(busy)   // replaced the loop with a condition which is evaluated only once
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notify();
}

Dans le cas où vous avez un grand nombre de threads, ou si la condition de la boucle d'attente est coûteuse à évaluer, notify()sera beaucoup plus rapide que notifyAll(). Par exemple, si vous avez 1000 threads, 999 threads seront réveillés et évalués après le premier notifyAll(), puis 998, puis 997, etc. Au contraire, avec la notify()solution, un seul fil sera réveillé.

À utiliser notifyAll()lorsque vous devez choisir le thread qui fera le travail suivant:

synchronized(this) {
    while(idx != last+1)  // wait until it's my turn
        wait();
}
...
synchronized(this) {
    last = idx;
    notifyAll();
}

Enfin, il est important de comprendre qu'en cas de notifyAll(), le code à l'intérieur des synchronizedblocs qui ont été réveillés sera exécuté séquentiellement, pas tous en même temps. Disons qu'il y a trois threads en attente dans l'exemple ci-dessus, et le quatrième thread appelle notifyAll(). Les trois threads seront réveillés mais un seul commencera l'exécution et vérifiera l'état de la whileboucle. Si la condition est true, il appellera à wait()nouveau, et alors seulement le deuxième thread commencera à s'exécuter et vérifiera sa whilecondition de boucle, et ainsi de suite.


4

Voici une explication plus simple:

Vous avez raison de dire que si vous utilisez notify () ou notifyAll (), le résultat immédiat est qu'exactement un autre thread va acquérir le moniteur et commencer à s'exécuter. (En supposant que certains threads ont en fait été bloqués lors de l'attente de cet objet, d'autres threads non liés n'absorbent pas tous les cœurs disponibles, etc.) L'impact survient plus tard.

Supposons que les threads A, B et C attendent sur cet objet et que le thread A récupère le moniteur. La différence réside dans ce qui se passe une fois que A libère le moniteur. Si vous avez utilisé notify (), alors B et C sont toujours bloqués dans wait (): ils n'attendent pas sur le moniteur, ils attendent d'être notifiés. Lorsque A relâche le moniteur, B et C seront toujours assis là, attendant une notification ().

Si vous avez utilisé notifyAll (), alors B et C ont tous deux avancé au-delà de l'état "attendre la notification" et attendent tous deux d'acquérir le moniteur. Lorsque A libère le moniteur, B ou C l'acquièrent (en supposant qu'aucun autre thread n'est en concurrence pour ce moniteur) et commence à s'exécuter.


Explication très claire. Le résultat de ce comportement de notify () peut conduire à un "signal manqué" / "notification manquée" conduisant à une situation de blocage / pas de progression de l'état de l'application. P-Producer, C-Consumer P1, P2 et C2 attendent C1. Les appels C1 notifient () et sont destinés à un producteur mais C2 peut être réveillé et donc P1 et P2 ont manqué la notification et ll attendra une autre "notification" explicite (un appel notify ()).
user104309

4

Cette réponse est une réécriture graphique et une simplification de l'excellente réponse de xagyg , y compris des commentaires par eran .

Pourquoi utiliser notifyAll, même lorsque chaque produit est destiné à un seul consommateur?

Considérez les producteurs et les consommateurs, simplifiés comme suit.

Producteur:

while (!empty) {
   wait() // on full
}
put()
notify()

Consommateur:

while (empty) {
   wait() // on empty
}
take()
notify()

Supposons que 2 producteurs et 2 consommateurs partagent un tampon de taille 1. L'image suivante illustre un scénario conduisant à un blocage , qui serait évité si tous les threads utilisés notifientAll .

Chaque notification est étiquetée avec le fil en cours de réveil.

impasse due à la notification


3

Je voudrais mentionner ce qui est expliqué dans Java Concurrency en pratique:

Premier point, que ce soit Notify ou NotifyAll?

It will be NotifyAll, and reason is that it will save from signall hijacking.

Si deux threads A et B attendent sur des prédicats de condition différents de la même file d'attente de conditions et qu'une notification est appelée, alors c'est jusqu'à la JVM que la JVM de thread notifiera.

Maintenant, si notifier était destiné au thread A et au thread notifié JVM B, le thread B se réveillera et verra que cette notification n'est pas utile, il attendra donc à nouveau. Et le fil A ne connaîtra jamais ce signal manqué et quelqu'un a détourné sa notification.

Ainsi, l'appel à notifyAll résoudra ce problème, mais encore une fois, cela aura un impact sur les performances car il avertira tous les threads et tous les threads rivaliseront pour le même verrou et cela impliquera un changement de contexte et donc une charge sur le processeur. Mais nous ne devons nous soucier des performances que si elles se comportent correctement, si leur comportement lui-même n'est pas correct, les performances ne sont d'aucune utilité.

Ce problème peut être résolu en utilisant l'objet Condition de verrouillage à verrouillage explicite, fourni dans jdk 5, car il fournit une attente différente pour chaque prédicat de condition. Ici, il se comportera correctement et il n'y aura pas de problème de performance car il appellera le signal et s'assurera qu'un seul thread attend cette condition


3

notify()- Sélectionne un thread aléatoire dans l'ensemble d'attente de l'objet et le met dans l' BLOCKEDétat. Les autres threads de l'ensemble d'attente de l'objet sont toujours dans leWAITING état.

notifyAll()- Déplace tous les threads de l'ensemble d'attente de l'objet à l' BLOCKEDétat. Une fois que vous l'utilisez notifyAll(), il n'y a plus de threads dans l'ensemble d'attente de l'objet partagé car tous sont maintenant en BLOCKEDétat et non dansWAITING état.

BLOCKED- bloqué pour l'acquisition du verrou. WAITING- en attente de notification (ou bloqué pour la fin de la jointure).


3

Tiré du blog sur Java efficace:

The notifyAll method should generally be used in preference to notify. 

If notify is used, great care must be taken to ensure liveness.

Donc, ce que je comprends (du blog susmentionné, commentaire de "Yann TM" sur la réponse acceptée et les documents Java ):

  • notify (): JVM réveille l'un des threads en attente sur cet objet. La sélection des fils se fait arbitrairement sans équité. Ainsi, le même fil peut être réveillé encore et encore. L'état du système change donc mais aucun progrès réel n'est réalisé. Créant ainsi un livelock .
  • notifyAll (): JVM réveille tous les threads, puis tous les threads se précipitent pour le verrou sur cet objet. Maintenant, le planificateur CPU sélectionne un thread qui acquiert un verrou sur cet objet. Ce processus de sélection serait bien meilleur que la sélection par JVM. Ainsi, assurant la vivacité.

2

Jetez un œil au code publié par @xagyg.

Supposons que deux threads différents attendent deux conditions différentes:
le premier thread attend buf.size() != MAX_SIZEet le second thread attendbuf.size() != 0 .

Supposons à un moment donné qu'il buf.size() n'est pas égal à 0 . La JVM appelle à la notify()place de notifyAll()et le premier thread est notifié (pas le second).

Le premier thread est réveillé, vérifie buf.size()ce qui pourrait revenir MAX_SIZEet revient à l'attente. Le deuxième thread n'est pas réveillé, continue d'attendre et n'appelle pas get().


1

notify()réveille le premier thread qui a appelé wait()le même objet.

notifyAll()réveille tous les threads qui ont appelé wait()le même objet.

Le thread de priorité la plus élevée s'exécutera en premier.


13
Dans le cas où notify()ce n'est pas exactement " le premier fil ".
Bhesh Gurung

6
vous ne pouvez pas prédire lequel sera choisi par VM. Dieu seul le sait.
Sergii Shevchyk

Il n'y a aucune garantie qui sera le premier (pas d'équité)
Ivan Voroshilin

Il ne réveillera le premier thread que si le système d'exploitation le garantit, et il est probable que non. Il s'en remet vraiment à l'OS (et à son ordonnanceur) pour déterminer quel thread se réveiller.
Paul Stelian

1

notifier notifiera seulement un thread en attente, tandis que notifier tous notifiera tous les threads en attente maintenant tous les threads notifiés et tous les threads bloqués sont éligibles pour le verrou, dont un seul obtiendra le verrou et tous les autres (y compris ceux qui sont en attente plus tôt) seront bloqués.


1

Pour résumer les excellentes explications détaillées ci-dessus, et de la manière la plus simple à laquelle je puisse penser, cela est dû aux limites du moniteur intégré JVM, qui 1) est acquis sur l'ensemble de l'unité de synchronisation (bloc ou objet) et 2) ne fait aucune discrimination quant à la condition spécifique attendue / notifiée le / à propos.

Cela signifie que si plusieurs threads attendent dans des conditions différentes et que notify () est utilisé, le thread sélectionné peut ne pas être celui qui ferait progresser la condition nouvellement remplie - provoquant ce thread (et d'autres threads actuellement en attente qui pourraient pour remplir la condition, etc.) pour ne pas pouvoir progresser, et éventuellement mourir de faim ou raccrocher un programme.

En revanche, notifyAll () permet à tous les threads en attente d'acquérir à nouveau le verrou et de vérifier leur état respectif, permettant ainsi éventuellement de progresser.

Donc notifier () ne peut être utilisé en toute sécurité que si un thread en attente est garanti pour permettre la progression s'il est sélectionné, ce qui est généralement satisfait lorsque tous les threads dans le même moniteur vérifient une seule et même condition - une situation assez rare cas dans des applications du monde réel.


0

Lorsque vous appelez l'attente () de l '"objet" (en attendant que le verrou d'objet soit acquis), interne cela libérera le verrou sur cet objet et aidera les autres threads à verrouiller cet "objet", dans ce scénario il y aura plus d'un thread en attente de la "ressource / objet" (étant donné que les autres threads ont également émis l'attente sur le même objet ci-dessus et en cours de route, il y aura un thread qui remplira la ressource / l'objet et invoquera notify / notifyAll).

Ici, lorsque vous émettez la notification du même objet (du même côté / de l'autre côté du processus / code), cela libérera un thread unique bloqué et en attente (pas tous les threads en attente - ce thread libéré sera choisi par JVM Thread Planificateur et tout le processus d'obtention de verrouillage sur l'objet est le même que normal).

Si vous n'avez qu'un seul thread qui partagera / travaillera sur cet objet, il est correct d'utiliser la méthode notify () seule dans votre implémentation wait-notify.

si vous êtes dans une situation où plus d'un thread lit et écrit sur des ressources / objets en fonction de votre logique métier, alors vous devriez opter pour notifyAll ()

maintenant je regarde comment exactement le jvm identifie et brise le thread en attente lorsque nous émettons notify () sur un objet ...


0

Bien qu'il y ait quelques réponses solides ci-dessus, je suis surpris par le nombre de confusions et de malentendus que j'ai lus. Cela prouve probablement l'idée que l'on devrait utiliser java.util.concurrent autant que possible au lieu d'essayer d'écrire son propre code concurrent cassé. Retour à la question: pour résumer, la meilleure pratique aujourd'hui consiste à ÉVITER de notifier () dans TOUTES les situations en raison du problème de réveil perdu. Quiconque ne comprend pas cela ne devrait pas être autorisé à écrire du code d'accès concurrentiel critique. Si vous êtes préoccupé par le problème de regroupement, un moyen sûr de réveiller un thread à la fois est de: 1. Construire une file d'attente explicite pour les threads en attente; 2. Demandez à chaque thread de la file d'attente d'attendre son prédécesseur; 3. Demandez à chaque appel de thread notifierAll () une fois terminé. Ou vous pouvez utiliser Java.util.concurrent. *,


d'après mon expérience, l'utilisation de l'attente / notification est souvent utilisée dans les mécanismes de file d'attente où un thread ( Runnableimplémentation) traite le contenu d'une file d'attente. Le wait()est ensuite utilisé chaque fois que la file d'attente est vide. Et le notify()est appelé lorsque des informations sont ajoutées. -> dans un tel cas, il n'y a qu'un seul thread qui appelle le wait(), alors cela ne semble pas un peu idiot d'utiliser un notifyAll()si vous savez qu'il n'y a qu'un seul thread en attente.
bvdb

-2

Réveiller tout n'a pas beaucoup d'importance ici. attendre notifier et notifier tous, tout cela est mis après avoir possédé le moniteur de l'objet. Si un thread est en attente et qu'un avis est appelé, ce thread prendra le verrou et aucun autre thread à ce stade ne pourra prendre ce verrou. L'accès simultané ne peut donc pas avoir lieu du tout. Pour autant que je sache, tout appel à attendre notifier et notifier ne peut être effectué qu'après avoir pris le verrou sur l'objet. Corrigez-moi si je me trompe.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.