Je sais que cette question a déjà été posée et que l'on y a répondu à de nombreuses reprises, mais je ne pouvais tout simplement pas comprendre les astuces trouvées sur Internet, telles que ceci ou cela un.
Ces deux solutions vérifient si les unités en attente de la variable notifyAll
dans la méthode put()
et vice-versa dans les méthodes get()
sont vides du tableau/de la file d'attente/de la liste liée de la file d'attente bloquante. Un commentaire dans le deuxième lien souligne cette situation et mentionne que ce n'est pas nécessaire.
Donc la question est; Il me semble aussi un peu étrange de vérifier si la file d'attente est vide | full pour notifier tous les threads en attente. Des idées?
Merci d'avance.
Je sais que c'est une vieille question à présent, mais après avoir lu la question et ses réponses, je ne pouvais pas m'en empêcher, j'espère que vous trouverez cela utile.
En ce qui concerne la vérification de la saturation de la file d'attente avant de notifier les autres threads en attente, il manque quelque chose qui correspond aux deux méthodes put (T t)
et T get()
sont toutes deux des méthodes synchronized
, ce qui signifie qu'un seul thread peut entrer une de ces méthodes à la fois. Cela ne les empêchera pas de fonctionner ensemble. Ainsi, si un thread-a a entré la méthode put (T t)
, un autre thread-b peut toujours entrer et commencer à exécuter les instructions de la méthode T get()
avant que thread-a ne soit sorti de put (T t)
, de sorte que la structure double-checking
fera en sorte que Les développeurs se sentent un peu plus en sécurité, car vous ne pouvez pas savoir si le changement de contexte du processeur aura lieu ou non.
Une approche meilleure et plus recommandée consiste à utiliser Reentrant Locks
et Conditions
:
// J'ai édité le code source à partir de ce link
Condition isFullCondition;
Condition isEmptyCondition;
Lock lock;
public BQueue() {
this(Integer.MAX_VALUE);
}
public BQueue(int limit) {
this.limit = limit;
lock = new ReentrantLock();
isFullCondition = lock.newCondition();
isEmptyCondition = lock.newCondition();
}
public void put (T t) {
lock.lock();
try {
while (isFull()) {
try {
isFullCondition.await();
} catch (InterruptedException ex) {}
}
q.add(t);
isEmptyCondition.signalAll();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
lock.lock();
try {
while (isEmpty()) {
try {
isEmptyCondition.await();
} catch (InterruptedException ex) {}
}
t = q.poll();
isFullCondition.signalAll();
} finally {
lock.unlock();
}
return t;
}
En utilisant cette approche, double checking
n'est pas nécessaire, car l'objet lock
est partagé entre les deux méthodes, ce qui signifie qu'un seul thread a ou b peut entrer n'importe laquelle de ces méthodes à la fois, contrairement aux méthodes synchronisées qui créent des moniteurs différents, et uniquement ces threads en attente, car la file d'attente est pleine sera notifiée quand il y aura plus d'espace, de même que pour les threads en attente car la file d'attente est vide, cela entraînera une meilleure utilisation du processeur. vous pouvez trouver des exemples plus détaillés avec le code source ici
Je pense logiquement qu'il n'y a pas de mal à faire cette vérification supplémentaire avant notifyAll()
.
Vous pouvez simplement notifyAll()
une fois que vous mettez/récupérez quelque chose dans la file d'attente. Tout fonctionnera toujours et votre code est plus court. Cependant, il n'y a pas non plus de mal à vérifier si quelqu'un attend potentiellement (en vérifiant si elle atteint la limite de la file d'attente) avant d'appeler notifyAll()
. Cette partie supplémentaire de la logique enregistre les invocations notifyAll()
inutiles.
Cela dépend simplement du fait que vous voulez un code plus court et plus propre ou que votre code s'exécute plus efficacement. (N'a pas examiné la mise en œuvre de notifyAll()
. S'il s'agit d'une opération peu coûteuse s'il n'y a personne en attente, le gain de performance peut ne pas être évident pour cette vérification supplémentaire de toute façon)
La raison pour laquelle les auteurs ont utilisé notifyAll()
est simple: ils ne savaient pas si c'était nécessaire ou non, ils ont donc opté pour l'option "plus sûr".
Dans l'exemple ci-dessus, il suffirait d'appeler notify()
car pour chaque élément ajouté, un seul thread en attente peut être servi dans toutes les circonstances.
Cela devient plus évident si votre file d'attente a également la possibilité d'ajouter plusieurs éléments en une étape, comme addAll(Collection<T> list)
, car dans ce cas, plusieurs threads en attente sur une liste vide pourraient être servis, pour être exact: autant de threads que d'éléments ont été ajouté.
La notifyAll()
entraîne toutefois une surcharge supplémentaire dans le cas particulier d'un élément unique, car de nombreux threads sont réveillés inutilement et doivent donc être de nouveau mis en veille, bloquant entre-temps l'accès à la file d'attente. Donc, remplacer notifyAll()
par notify()
améliorerait la vitesse dans ce cas particulier .
Mais alors not using wait/notify et synchronisé du tout, mais au lieu de cela, l'utilisation du paquetage simultané augmenterait la vitesse de beaucoup plus que ne le ferait une implémentation intelligente wait/notify.