Comment atomique/volatile/synchronisé fonctionne-t-il 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++;
}
volatile
fonctionne-t-il 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 un bloc synchronisé en même temps ... ai-je raison? Si cela est vrai, comment atomic.incrementAndGet()
fonctionne-t-il sans synchronized
? Et est-ce thread-safe?
Et quelle est la différence entre la lecture interne et l'écriture dans des variables volatiles/variables atomiques? J'ai lu dans certains articles que le fil a une copie locale des variables - qu'est-ce que c'est?
Vous vous demandez précisément comment ils travaillent en interne, alors voici:
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
En gros, il lit une valeur dans la mémoire, l’incrémente et la remet en mémoire. Cela fonctionne en thread simple, mais de nos jours, à l'ère des caches multi-cœurs, multi-processeurs et multi-niveaux, cela ne fonctionnera pas correctement. Tout d'abord, il introduit une condition de concurrence critique (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 de la CPU "locale" (une partie du cache) et ne pas être visible pour les autres CPU/cœurs (et donc les threads). C'est pourquoi beaucoup font référence à copie locale d'une variable dans un thread. C'est très dangereux. Considérez ce code qui stoppe les threads, populaire mais cassé:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
Ajoutez volatile
à la variable stopped
et tout fonctionnera correctement. Si un autre thread modifie la variable stopped
via la méthode pleaseStop()
, vous êtes assuré de voir ce changement immédiatement dans la boucle while(!stopped)
du thread en cours de travail. En passant, ce n’est pas un bon moyen d’interrompre un thread, voir: Comment arrêter un thread qui fonctionne à tout jamais sans aucune utilisation et Arrêter un thread Java spécifique .
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
La classe AtomicInteger
utilise des opérations de processeur de bas niveau CAS ( compare-and-swap ) (aucune synchronisation n'est requise!). Elles vous permettent de modifier une variable particulière uniquement si la valeur actuelle est égale à quelque chose d'autre (et est renvoyé avec succès ). Ainsi, lorsque vous exécutez getAndIncrement()
, il s'exécutera en boucle (implémentation réelle simplifiée):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
Donc, fondamentalement: lisez; essayez de stocker la valeur incrémentée; en cas d'échec (la valeur n'est plus égale à current
), lisez et réessayez. La compareAndSet()
est implémentée en code natif (Assembly).
volatile
sans synchronisationprivate volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Ce code n'est pas correct. Il corrige le problème de visibilité (volatile
s'assure que les autres threads peuvent voir la modification apportée à counter
) mais a toujours une condition de concurrence critique. Cela a été expliqué plusieurs fois: la pré/post-incrémentation n’est pas atomique.
Le seul effet secondaire de volatile
est "vidage" des caches afin 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 pourquoi volatile
n'est pas par défaut.
volatile
sans synchronisation (2)volatile int i = 0;
void incIBy5() {
i += 5;
}
Le même problème que ci-dessus, mais encore pire car i
n'est pas private
. La condition de concurrence 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.
synchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Surprise, ce code est également incorrect. En fait, c'est complètement faux. Tout d’abord, vous synchronisez sur i
, qui est sur le point d’être modifié (de plus, i
est une primitive, je suppose donc que vous synchronisez sur une Integer
temporaire créée via la méthode de substitution automatique ...) Complètement imparfait. Vous pouvez aussi écrire:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
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, donc synchronized
n'a 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 lire i
à temp
de façon synchrone (ayant la même valeur localement dans temp
), puis le premier attribue une nouvelle valeur à i
(disons 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'une int
est atomique) et la seconde également. À mon avis, ce sont les formes correctes:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}
Déclarer une variable en tant que volatile signifie que la modification de sa valeur affecte immédiatement le stockage en mémoire réel de 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 atomic garantit que les opérations effectuées sur la variable se déroulent de manière atomique, c'est-à-dire que toutes les sous-étapes de l'opération sont terminées dans le thread où 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 complétées comme s'il s'agissait d'une seule opération indivisible/ininterruptible.
Synchronizing tous les accès à une variable permettent à un seul thread à la fois d'accéder à la variable et obligent tous les autres threads à attendre que ce thread accédant 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. En outre, il est tout à fait possible de synchroniser uniquement certains accès à une variable et de permettre à d’autres accès d’être désynchronisés (par exemple, synchroniser toutes les écritures sur une variable, mais aucune des lectures qu’elle contient).
Atomicité, synchronisation et volatilité sont des attributs indépendants, mais sont généralement utilisés en combinaison pour imposer la coopération de thread appropriée pour l'accès aux variables.
Addendum (avril 2016)
L'accès synchronisé à une variable est généralement implémenté à l'aide de monitor ou semaphore. Il s'agit de mécanismes mutex (exclusion mutuelle) de bas niveau qui permettent à un thread d'acquérir le contrôle exclusif d'une variable ou d'un bloc de code, ce qui oblige 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 a lieu sur un objet . Cela signifie que l'appel d'une méthode synchronisée d'une classe verrouillera l'objet this
de l'appel. Les méthodes synchronisées statiques verrouillera l'objet Class
lui-même.
De même, la saisie d'un bloc synchronisé nécessite le verrouillage de l'objet this
de la méthode.
Cela signifie qu’une méthode synchronisée (ou un bloc) peut être exécutée simultanément dans plusieurs threads s’ils se verrouillent sur des objets different, mais qu’un seul thread peut exécuter une méthode synchronisée (ou un bloc) à la fois donné single objet.
volatile:
volatile
est un mot clé. volatile
oblige tous les threads à obtenir la dernière valeur de la variable à partir 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 à la valeur de la variable volatile en même temps.
L'utilisation de variables volatile
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 variable volatile
sont toujours visibles par les autres threads. De plus, cela signifie également que lorsqu'un thread lit une variable volatile
, il ne voit pas seulement la dernière modification de la variable volatile, mais également les effets secondaires du code qui a conduit à cette modification.
Quand utiliser: un thread modifie les données et d'autres threads doivent lire la dernière valeur de données. Les autres threads prendront des mesures mais ne mettront pas à jour les données.
AtomicXXX:
Les classes AtomicXXX
prennent en charge la programmation thread-safe sans verrou sur des variables uniques. Ces classes AtomicXXX
(comme AtomicInteger
) résolvent les erreurs d’incohérence dans la mémoire et les effets secondaires de la modification de variables volatiles, auxquelles plusieurs utilisateurs ont accédé.
Quand utiliser: plusieurs threads peuvent lire et modifier des données.
synchronized:
synchronized
est le mot clé utilisé pour protéger une méthode ou un bloc de code. En rendant la méthode synchronisée, cela a deux effets:
Premièrement, il n'est pas possible que deux invocations de méthodes synchronized
sur le même objet s'entrelacent. Lorsqu'un thread exécute une méthode synchronized
pour un objet, tous les autres threads qui invoquent des méthodes synchronized
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 méthode synchronized
est fermée, elle établit automatiquement une relation «passe-avant» avec tout appel ultérieur d'une méthode synchronized
pour le même objet. Cela garantit que les modifications apportées à l'état de l'objet sont visibles par tous les threads.
Quand 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 équivalent à volatile + synchronized
même si l'implémentation est différente. AmtomicXXX
étend volatile
variables + compareAndSet
méthodes mais n'utilise pas la synchronisation.
Questions SE connexes:
Différence entre volatile et synchronisé en Java
Volatile boolean 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
Je sais que deux threads ne peuvent pas entrer dans le bloc Synchronize en même temps
Deux threads ne peuvent pas entrer deux fois un bloc synchronisé sur le même objet. Cela signifie que deux threads peuvent entrer le même bloc sur des objets différents. Cette confusion peut conduire à un code comme celui-ci.
private Integer i = 0;
synchronized(i) {
i++;
}
Cela ne se comportera pas comme prévu car il pourrait être verrouillé sur un objet différent à chaque fois.
si cela est vrai, comment cette atomic.incrementAndGet () fonctionne sans Synchronize? et est le thread safe ??
oui. Il n'utilise pas le verrouillage pour assurer la sécurité du fil.
Si vous voulez savoir comment ils fonctionnent plus en détail, vous pouvez lire le code correspondant.
Et quelle est la différence entre la lecture interne et l'écriture dans une variable variable/variable atomique?
La classe atomique utilise des champs volatiles. Il n'y a pas de différence sur le terrain. La différence est les opérations effectuées. Les classes Atomic utilisent les opérations CompareAndSwap ou CAS.
j'ai lu dans certains articles que ce fil a une copie locale de variables qu'est-ce que c'est ??
Je ne peux que supposer que cela fait référence au fait que chaque processeur a sa propre vue en mémoire cache qui peut être différente de chaque autre processeur. Pour 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.
Une synchronisation volatile + est une solution infaillible pour une opération (instruction) entièrement atomique qui inclut plusieurs instructions à la CPU.
Dites par exemple: volatile int i = 2; i ++, qui n'est rien d'autre que i = i + 1; qui fait de i la valeur 3 dans la mémoire après l’exécution de cette instruction .Cela comprend la lecture de la valeur existante dans la mémoire pour i (qui est 2), la charge dans le registre de l’accumulateur de valeur existante avec un (2 + 1 = 3 dans l’accumulateur), puis réécrivez cette valeur incrémentée dans la mémoire. Ces opérations ne sont pas assez atomiques bien que la valeur de i soit volatile. Être volatile garantit uniquement qu'un SINGLE lecture/écriture à partir de la mémoire est atomique et non avec MULTIPLE. Par conséquent, nous devons également avoir synchronisé autour de i ++ pour le conserver comme déclaration atomique infaillible. Rappelez-vous le fait qu'une déclaration comprend plusieurs déclarations.
J'espère que l'explication est suffisamment claire.
Synchronized Vs Atomic Vs Volatile:
1. Volatile et Atomic s'appliquent uniquement à une variable, While Synchronized s'applique à la méthode .
2. Volatile assure la visibilité et non l’atmicité/la cohérence de l’objet, Tandis que les deux autres garantissent la visibilité et l’atomicité .
3. Variable volatile stockée dans RAM et son accès est plus rapide, mais nous ne pouvons pas atteindre la sécurité ni la synchronisation des threads sans mot-clé synchronisé .
4. Synchronized implémenté en tant que bloc synchronisé ou méthode synchronisée alors que les deux ne le sont pas. Nous pouvons mettre en sécurité plusieurs lignes de code à l’aide d’un mot-clé synchronisé, mais nous ne pouvons pas obtenir le même résultat .
5. Synchronized peut verrouiller le même objet de classe ou un objet de classe différent alors que les deux ne peuvent pas .
S'il vous plaît corrigez-moi si quelque chose m'a manqué.
Le modificateur Java volatile est un exemple de mécanisme spécial permettant de garantir la communication entre les threads. Lorsqu'un thread écrit dans une variable volatile et qu'un autre thread voit cette écriture, le premier thread informe la seconde de tout le contenu de la mémoire jusqu'à ce qu'il ait effectué l'écriture dans cette variable volatile.
Les opérations atomiques sont effectuées dans une seule unité de tâche sans interférence d'autres opérations. Les opérations atomiques sont une nécessité dans un environnement multi-thread afin d'éviter toute incohérence dans les données.