Que fait AtomicBoolean qu’un booléen volatil ne peut pas réaliser?
Ils sont juste totalement différents. Prenons cet exemple d'un entier volatile
:
volatile int i = 0;
void incIBy5() {
i += 5;
}
Si deux threads appellent la fonction simultanément, i
pourrait être 5 après, car le code compilé ressemblera à celui-ci (sauf que vous ne pouvez pas synchroniser sur int
):
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Si une variable est volatile, chaque accès atomique est synchronisé, mais il n’est pas toujours évident de déterminer ce qui peut être qualifié d’accès atomique. Avec un objet Atomic*
, il est garanti que chaque méthode est "atomique".
Ainsi, si vous utilisez AtomicInteger
et getAndAdd(int delta)
, vous pouvez être sûr que le résultat sera 10
. De la même manière, si deux threads nient simultanément une variable boolean
, avec un AtomicBoolean
, vous pouvez être sûr qu'elle aura la valeur d'origine par la suite, avec un volatile boolean
, vous ne pourrez pas.
Donc chaque fois que vous avez plusieurs threads en train de modifier un champ, vous devez le rendre atomique ou utiliser une synchronisation explicite.
Le but de volatile
est différent. Considérez cet exemple
volatile boolean stop = false;
void loop() {
while (!stop) { ... }
}
void stop() { stop = true; }
Si vous avez un thread exécutant loop()
et un autre thread appelant stop()
, vous pouvez vous heurter à une boucle infinie si vous omettez volatile
, car le premier thread pourrait mettre la valeur en cache d'arrêter. Ici, le volatile
indique au compilateur d’être un peu plus prudent avec les optimisations.
J'utilise des champs volatiles lorsque ledit champ est UNIQUEMENT mis à jour par son thread propriétaire et que la valeur n'est lue que par d'autres threads. Vous pouvez le considérer comme un scénario de publication/abonnement dans lequel il y a de nombreux observateurs mais un seul éditeur. Cependant, si ces observateurs doivent effectuer une certaine logique en fonction de la valeur du champ puis repousser une nouvelle valeur, alors je vais avec Atomic *, verrous ou blocs synchronisés, ce qui me convient le mieux. Dans de nombreux scénarios simultanés, il s'agit d'obtenir la valeur, de la comparer à une autre et de la mettre à jour si nécessaire, d'où les méthodes compareAndSet et getAndSet présentes dans les classes Atomic *.
Consultez les JavaDocs du package Java.util.concurrent.atomic pour obtenir une liste des classes Atomic et une excellente explication de leur fonctionnement (elles viennent d'apprendre qu'elles sont sans verrou, elles ont donc un avantage sur serrures ou blocs synchronisés)
Vous ne pouvez pas utiliser compareAndSet
, getAndSet
comme opération atomique avec un booléen volatil (à moins bien sûr que vous ne le synchronisiez).
AtomicBoolean
a des méthodes qui effectuent leurs opérations composées de manière atomique et sans avoir à utiliser un bloc synchronized
. D'autre part, volatile boolean
ne peut effectuer des opérations composées que si cela est fait dans un bloc synchronized
.
Les effets de mémoire de la lecture/écriture à volatile boolean
sont identiques aux méthodes get
et set
de AtomicBoolean
respectivement.
Par exemple, la méthode compareAndSet
effectuera atomiquement les opérations suivantes (sans bloc synchronized
):
if (value == expectedValue) {
value = newValue;
return true;
} else {
return false;
}
Par conséquent, la méthode compareAndSet
vous permettra d'écrire du code dont l'exécution est garantie, une seule fois, même si elle est appelée à partir de plusieurs threads. Par exemple:
final AtomicBoolean isJobDone = new AtomicBoolean(false);
...
if (isJobDone.compareAndSet(false, true)) {
listener.notifyJobDone();
}
Est garanti de ne notifier l’auditeur qu’une seule fois (en supposant qu’aucun autre thread ne redéfinit le AtomicBoolean
sur false
après qu’il a été défini sur true
).
volatile
mot-clé garantit que se passe avant la relation entre les threads partageant cette variable. Cela ne vous garantit pas que 2 threads ou plus ne s'interrompent pas lorsqu'ils accèdent à cette variable booléenne.
S'il existe plusieurs threads accédant à une variable de niveau classe, chaque thread peut conserver une copie de cette variable dans son cache de threadlocal.
Rendre la variable volatile empêchera les threads de conserver la copie de variable dans le cache threadlocal.
Les variables atomiques sont différentes et permettent la modification atomique de leurs valeurs.
Booléen Volatile vs AtomicBoolean
Les classes Atomic * enveloppent une primitive volatile du même type. De la source:
public class AtomicLong extends Number implements Java.io.Serializable {
...
private volatile long value;
...
public final long get() {
return value;
}
...
public final void set(long newValue) {
value = newValue;
}
Donc, si tout ce que vous faites est d’obtenir et de configurer un Atomic *, vous pouvez aussi bien avoir un champ volatile à la place.
Que fait AtomicBoolean qu’un booléen volatil ne peut pas réaliser?
Les classes Atomic * vous fournissent des méthodes offrant des fonctionnalités plus avancées telles que incrementAndGet()
, compareAndSet()
et d'autres qui implémentent plusieurs opérations (obtenir/incrémenter/définir, tester/définir) sans verrouillage. C'est pourquoi les classes Atomic * sont si puissantes.
Par exemple, si plusieurs threads utilisent le code suivant à l'aide de ++
, Il y aura des conditions de concurrence car ++
Est en réalité: get, increment et set.
private volatile value;
...
// race conditions here
value++;
Toutefois, le code suivant fonctionnera dans un environnement multi-thread en toute sécurité, sans verrou:
private final AtomicLong value = new AtomicLong();
...
value.incrementAndGet();
Il est également important de noter que le fait d'encapsuler votre champ volatile à l'aide de la classe Atomic * constitue un bon moyen d'encapsuler la ressource partagée critique du point de vue d'un objet. Cela signifie que les développeurs ne peuvent tout simplement pas traiter le champ en supposant qu'il ne soit pas partagé, ce qui risque d'injecter des problèmes avec un champ ++; ou un autre code introduisant des conditions de concurrence.
Le type primitif booléen est atomique pour les opérations d'écriture et de lecture, volatile garantit le principe d'événement précédent. Donc, si vous avez besoin de simples get () et set (), vous n'avez pas besoin de AtomicBoolean.
Par contre, si vous devez implémenter une vérification avant de définir la valeur d’une variable, par exemple. "si true, alors défini sur false", vous devez également effectuer cette opération de manière atomique. Dans ce cas, utilisez compareAndSet et d'autres méthodes fournies par AtomicBoolean, car si vous essayez d'implémenter cette logique avec des valeurs booléennes volatiles, vous aurez besoin d'une certaine synchronisation pour assurez-vous que la valeur n'a pas changé entre get et set.
Rappelez-vous l'IDIOM -
LIRE - MODIFIER - ÉCRIRE ce que tu ne peux pas réaliser avec volatile
Si vous n'avez qu'un seul thread modifiant votre booléen, vous pouvez utiliser un booléen volatil (vous le faites généralement pour définir une variable stop
cochée dans la boucle principale du thread).
Cependant, si vous avez plusieurs threads modifiant le booléen, vous devez utiliser un AtomicBoolean
. Sinon, le code suivant n'est pas sûr:
boolean r = !myVolatileBoolean;
Cette opération se fait en deux étapes:
Si un autre thread modifie la valeur entre #1
et 2#
, vous pourriez avoir un mauvais résultat. Les méthodes AtomicBoolean
évitent ce problème en effectuant les étapes #1
et #2
atomiquement.