Comment fonctionne atomique / volatile / synchronisé en interne?
Quelle est la différence entre les blocs de code suivants?
Code 1
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Code 2
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Code 3
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Fonctionne volatile
de la manière suivante? Est
volatile int i = 0;
void incIBy5() {
i += 5;
}
équivalent à
Integer i = 5;
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Je pense que deux threads ne peuvent pas entrer dans un bloc synchronisé en même temps ... ai-je raison? Si cela est vrai, alors comment atomic.incrementAndGet()
fonctionne sans synchronized
? Et est-il thread-safe?
Et quelle est la différence entre la lecture interne et l'écriture sur des variables volatiles / variables atomiques? J'ai lu dans un article que le thread a une copie locale des variables - qu'est-ce que c'est?
Réponses:
Vous demandez spécifiquement comment ils fonctionnent en interne , alors vous êtes ici:
Pas de synchronisation
Il lit essentiellement la valeur de la mémoire, l'incrémente et la remet en mémoire. Cela fonctionne sur un seul thread, mais de nos jours, à l'ère des caches multicœurs, multi-CPU et multi-niveaux, cela ne fonctionnera pas correctement. Tout d'abord, il introduit la condition de concurrence (plusieurs threads peuvent lire la valeur en même temps), mais aussi des problèmes de visibilité. La valeur peut uniquement être stockée dans la mémoire CPU " locale " (un cache) et ne pas être visible pour les autres CPU / cœurs (et donc - les threads). C'est pourquoi beaucoup font référence à une copie locale d'une variable dans un thread. C'est très dangereux. Considérez ce code d'arrêt de fil populaire mais cassé:
Ajoutez
volatile
à lastopped
variable et cela fonctionne très bien - si tout autre thread modifie lastopped
variable via lapleaseStop()
méthode, vous êtes assuré de voir ce changement immédiatement dans lawhile(!stopped)
boucle du thread de travail . BTW ce n'est pas non plus un bon moyen d'interrompre un thread, voir: Comment arrêter un thread qui s'exécute pour toujours sans aucune utilisation et Arrêter un thread java spécifique .AtomicInteger
La
AtomicInteger
classe utilise des opérations CPU de bas niveau CAS ( comparer et échanger ) (aucune synchronisation nécessaire!). Elles vous permettent de modifier une variable particulière uniquement si la valeur actuelle est égale à autre chose (et est retournée avec succès). Ainsi, lorsque vous l'exécutez,getAndIncrement()
il s'exécute en fait en boucle (implémentation réelle simplifiée):Donc, fondamentalement: lire; essayez de stocker la valeur incrémentée; en cas d'échec (la valeur n'est plus égale à
current
), lisez et réessayez. LecompareAndSet()
est implémenté en code natif (assembly).volatile
sans synchronisationCe code n'est pas correct. Il corrige le problème de visibilité (
volatile
garantit que les autres threads peuvent voir les modifications apportéescounter
), mais a toujours une condition de concurrence. Cela a été expliqué plusieurs fois: la pré / post-incrémentation n'est pas atomique.Le seul effet secondaire de
volatile
" vidage " des caches pour que toutes les autres parties voient la version la plus récente des données. C'est trop strict dans la plupart des situations; c'est pourquoivolatile
n'est pas par défaut.volatile
sans synchronisation (2)Le même problème que ci-dessus, mais encore pire car ce
i
n'est pas le casprivate
. La condition de course est toujours présente. Pourquoi est-ce un problème? Si, par exemple, deux threads exécutent ce code simultanément, la sortie peut être+ 5
ou+ 10
. Cependant, vous êtes assuré de voir le changement.Multiple indépendant
synchronized
Surprise, ce code est également incorrect. En fait, c'est complètement faux. Tout d'abord, vous synchronisez
i
, ce qui est sur le point d'être modifié (en plus,i
c'est une primitive, donc je suppose que vous synchronisez sur un temporaireInteger
créé via l'autoboxing ...) Complètement défectueux. Vous pouvez également écrire:Deux threads ne peuvent pas entrer dans le même
synchronized
bloc avec le même verrou . Dans ce cas (et de la même manière dans votre code), l'objet verrou change à chaque exécution, doncsynchronized
n'a effectivement aucun effet.Même si vous avez utilisé une variable finale (ou
this
) pour la synchronisation, le code est toujours incorrect. Deux threads peuvent d'abord lirei
de façontemp
synchrone (ayant la même valeur localementtemp
), puis le premier attribue une nouvelle valeur ài
(par exemple, de 1 à 6) et l'autre fait la même chose (de 1 à 6).La synchronisation doit s'étendre de la lecture à l'attribution d'une valeur. Votre première synchronisation n'a aucun effet (la lecture d'un
int
est atomique) et la seconde également. À mon avis, ce sont les formes correctes:la source
compareAndSet
est juste une mince enveloppe autour du fonctionnement CAS. J'entre dans certains détails dans ma réponse.Déclarer une variable comme volatile signifie que la modification de sa valeur affecte immédiatement le stockage de mémoire réel pour la variable. Le compilateur ne peut pas optimiser les références faites à la variable. Cela garantit que lorsqu'un thread modifie la variable, tous les autres threads voient immédiatement la nouvelle valeur. (Ceci n'est pas garanti pour les variables non volatiles.)
La déclaration d'une variable atomique garantit que les opérations effectuées sur la variable se produisent de manière atomique, c'est-à-dire que toutes les sous-étapes de l'opération sont terminées dans le thread, elles sont exécutées et ne sont pas interrompues par d'autres threads. Par exemple, une opération d'incrémentation et de test nécessite que la variable soit incrémentée puis comparée à une autre valeur; une opération atomique garantit que ces deux étapes seront accomplies comme s'il s'agissait d'une seule opération indivisible / sans interruption.
La synchronisation de tous les accès à une variable permet à un seul thread à la fois d'accéder à la variable et force tous les autres threads à attendre que ce thread d'accès libère son accès à la variable.
L'accès synchronisé est similaire à l'accès atomique, mais les opérations atomiques sont généralement implémentées à un niveau de programmation inférieur. De plus, il est tout à fait possible de synchroniser uniquement certains accès à une variable et de permettre à d'autres accès d'être non synchronisés (par exemple, synchroniser toutes les écritures dans une variable mais aucune des lectures de celle-ci).
L'atomicité, la synchronisation et la volatilité sont des attributs indépendants, mais sont généralement utilisés en combinaison pour appliquer une coopération de thread appropriée pour accéder aux variables.
Addendum (avril 2016)
L'accès synchronisé à une variable est généralement implémenté à l'aide d'un moniteur ou d'un sémaphore . Ce sont des mécanismes de mutex (exclusion mutuelle) de bas niveau qui permettent à un thread d'acquérir le contrôle d'une variable ou d'un bloc de code exclusivement, forçant tous les autres threads à attendre s'ils tentent également d'acquérir le même mutex. Une fois que le thread propriétaire libère le mutex, un autre thread peut acquérir le mutex à son tour.
Addendum (juillet 2016)
La synchronisation se produit sur un objet . Cela signifie que l'appel d'une méthode synchronisée d'une classe verrouillera l'
this
objet de l'appel. Les méthodes synchronisées statiques verrouillent l'Class
objet lui-même.De même, entrer un bloc synchronisé nécessite de verrouiller l'
this
objet de la méthode.Cela signifie qu'une méthode (ou un bloc) synchronisée peut s'exécuter dans plusieurs threads en même temps si elles se verrouillent sur des objets différents , mais qu'un seul thread peut exécuter une méthode (ou un bloc) synchronisée à la fois pour un seul objet donné .
la source
volatil:
volatile
est un mot-clé.volatile
force tous les threads à obtenir la dernière valeur de la variable de la mémoire principale au lieu du cache. Aucun verrouillage n'est requis pour accéder aux variables volatiles. Tous les threads peuvent accéder simultanément à une valeur de variable volatile.L'utilisation de
volatile
variables réduit le risque d'erreurs de cohérence de la mémoire, car toute écriture dans une variable volatile établit une relation passe-avant avec les lectures suivantes de cette même variable.Cela signifie que les modifications apportées à une
volatile
variable sont toujours visibles pour les autres threads . De plus, cela signifie également que lorsqu'un thread lit unevolatile
variable, il voit non seulement la dernière modification de la volatile, mais également les effets secondaires du code qui a conduit à la modification .Quand l'utiliser: un thread modifie les données et les autres threads doivent lire la dernière valeur des données. D'autres threads prendront des mesures mais ils ne mettront pas à jour les données .
AtomicXXX:
AtomicXXX
Les classes prennent en charge la programmation sans verrous et sans thread sur des variables uniques. CesAtomicXXX
classes (commeAtomicInteger
) résolvent les erreurs d'incohérence de la mémoire / les effets secondaires de la modification des variables volatiles, qui ont été accessibles dans plusieurs threads.Quand l'utiliser: plusieurs threads peuvent lire et modifier des données.
synchronisé:
synchronized
est un mot clé utilisé pour protéger une méthode ou un bloc de code. La méthode synchronisée a deux effets:Premièrement, il n'est pas possible que deux invocations de
synchronized
méthodes sur le même objet s'entrelacent. Lorsqu'un thread exécute unesynchronized
méthode pour un objet, tous les autres threads qui appellent dessynchronized
méthodes pour le même bloc d'objet (suspendre l'exécution) jusqu'à ce que le premier thread soit terminé avec l'objet.Deuxièmement, lorsqu'une
synchronized
méthode se termine, elle établit automatiquement une relation passe-avant avec toute invocation ultérieure d'unesynchronized
méthode pour le même objet. Cela garantit que les modifications de l'état de l'objet sont visibles pour tous les threads.Quand l'utiliser: plusieurs threads peuvent lire et modifier des données. Votre logique métier met non seulement à jour les données mais exécute également des opérations atomiques
AtomicXXX
est équivalentvolatile + synchronized
même si l'implémentation est différente.AmtomicXXX
étend lesvolatile
variables +compareAndSet
méthodes mais n'utilise pas la synchronisation.Questions SE connexes:
Différence entre volatile et synchronisé en Java
Volatile booléen vs AtomicBoolean
Bons articles à lire: (Le contenu ci-dessus est tiré de ces pages de documentation)
https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
la source
Deux threads ne peuvent pas entrer deux fois un bloc synchronisé sur le même objet. Cela signifie que deux threads peuvent entrer dans le même bloc sur des objets différents. Cette confusion peut conduire à un code comme celui-ci.
Cela ne se comportera pas comme prévu car il pourrait se bloquer à chaque fois sur un objet différent.
Oui. Il n'utilise pas de verrouillage pour assurer la sécurité du fil.
Si vous voulez savoir comment ils fonctionnent plus en détail, vous pouvez lire le code pour eux.
La classe atomique utilise des champs volatils . Il n'y a aucune différence sur le terrain. La différence réside dans les opérations effectuées. Les classes Atomic utilisent des opérations CompareAndSwap ou CAS.
Je ne peux que supposer que cela se réfère au fait que chaque CPU a sa propre vue en mémoire cache qui peut être différente de toutes les autres CPU. Pour vous assurer que votre processeur dispose d'une vue cohérente des données, vous devez utiliser des techniques de sécurité des threads.
Ce n'est un problème que lorsque la mémoire est partagée, au moins un thread la met à jour.
la source
Synchronized Vs Atomic Vs Volatile:
Veuillez me corriger si je manque quelque chose.
la source
Une synchronisation volatile + est une solution à toute épreuve pour qu'une opération (instruction) soit entièrement atomique qui comprend plusieurs instructions à la CPU.
Dites par exemple: volatile int i = 2; i ++, qui n'est rien d'autre que i = i + 1; ce qui fait de i la valeur 3 dans la mémoire après l'exécution de cette instruction. Cela inclut la lecture de la valeur existante de la mémoire pour i (qui est 2), la charge dans le registre de l'accumulateur CPU et fait le calcul en incrémentant la valeur existante avec un (2 + 1 = 3 dans l'accumulateur), puis réécris cette valeur incrémentée retour à la mémoire. Ces opérations ne sont pas assez atomiques bien que la valeur de i soit volatile. i étant volatile garantit seulement qu'une seule lecture / écriture de la mémoire est atomique et non avec MULTIPLE. Par conséquent, nous devons également avoir synchronisé autour d'i ++ pour qu'il reste une déclaration atomique à toute épreuve. N'oubliez pas qu'une déclaration comprend plusieurs déclarations.
J'espère que l'explication est suffisamment claire.
la source
Le modificateur volatile Java est un exemple de mécanisme spécial pour garantir que la communication se produit entre les threads. Lorsqu'un thread écrit dans une variable volatile et qu'un autre thread voit cette écriture, le premier thread informe le second de tout le contenu de la mémoire jusqu'à ce qu'il effectue l'écriture dans cette variable volatile.
Les opérations atomiques sont effectuées dans une seule unité de tâche sans interférence avec d'autres opérations. Les opérations atomiques sont nécessaires dans un environnement multi-thread pour éviter l'incohérence des données.
la source