Clairement, notify
réveille (n'importe quel) un thread de l'ensemble d'attente, notifyAll
réveille tous les threads de l'ensemble d'attente. La discussion suivante devrait dissiper tous les doutes. notifyAll
doit ê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 while
boucle 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' wait
appel). 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 while
boucle, 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 IndexArrayOutOfBoundsException
comme 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 put
méthode ainsi que dans la get
mé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 get
méthode
- C3 tente d'obtenir 1 caractère - blocs à l'entrée de la get
méthode
ÉTAPE 5:
- C1 exécute la get
méthode - obtient la méthode char, calls notify
, exit
- Le notify
ré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 put
mé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 notify
par notifyAll
dans le code producteur / consommateur (ci-dessus).