Quelle est la différence entre atomique / volatile / synchronisé?

297

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 volatilede 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?

hardik
la source
5
Cela pose beaucoup de questions, avec du code qui ne compile même pas. Vous devriez peut-être lire un bon livre, comme Java Concurrency in Practice.
JB Nizet
4
@JBNizet vous avez raison !!! j'ai ce livre, il n'a pas le concept atomique en bref et je ne comprends pas certains concepts de cela. de malédiction c'est mon erreur pas d'auteur.
hardik
4
Vous n'avez pas vraiment à vous soucier de la façon dont il est implémenté (et cela varie selon le système d'exploitation). Ce que vous devez comprendre, c'est le contrat: la valeur est incrémentée atomiquement et tous les autres threads sont garantis pour voir la nouvelle valeur.
JB Nizet

Réponses:

392

Vous demandez spécifiquement comment ils fonctionnent en interne , alors vous êtes ici:

Pas de synchronisation

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

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é:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

Ajoutez volatileà la stoppedvariable et cela fonctionne très bien - si tout autre thread modifie la stoppedvariable via la pleaseStop()méthode, vous êtes assuré de voir ce changement immédiatement dans la while(!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

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

La AtomicIntegerclasse 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):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

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. Le compareAndSet()est implémenté en code natif (assembly).

volatile sans synchronisation

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Ce code n'est pas correct. Il corrige le problème de visibilité ( volatilegarantit que les autres threads peuvent voir les modifications apportées counter), 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 pourquoi volatilen'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 ce in'est pas le cas private. 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 + 5ou + 10. Cependant, vous êtes assuré de voir le changement.

Multiple indépendant 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 i, ce qui est sur le point d'être modifié (en plus, ic'est une primitive, donc je suppose que vous synchronisez sur un temporaire Integercréé via l'autoboxing ...) Complètement défectueux. Vous pouvez également écrire:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

Deux threads ne peuvent pas entrer dans le même synchronizedbloc 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 synchronizedn'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 lire ide façon tempsynchrone (ayant la même valeur localement temp), 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 intest 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;
  }
}
Tomasz Nurkiewicz
la source
10
La seule chose que j'ajouterais est que la JVM copie les valeurs des variables dans des registres pour les exploiter. Cela signifie que les threads s'exécutant sur un seul processeur / cœur peuvent toujours voir des valeurs différentes pour une variable non volatile.
David Harkness
@thomasz: est compareAndSet (courant, courant + 1) synchronisé ?? si non, que se passe-t-il lorsque deux threads exécutent cette méthode en même temps ??
hardik
@Hardik: compareAndSetest juste une mince enveloppe autour du fonctionnement CAS. J'entre dans certains détails dans ma réponse.
Tomasz Nurkiewicz
1
@thomsasz: ok, je passe par cette question de lien et répond par jon skeet, il dit "le thread ne peut pas lire une variable volatile sans vérifier si un autre thread a effectué une écriture." mais que se passe-t-il si un thread se trouve entre l'opération d'écriture et le deuxième thread le lit !! ai-je tort ?? n'est-ce pas une condition de concurrence sur le fonctionnement atomique
hardik
3
@Hardik: veuillez créer une autre question pour obtenir plus de réponses sur ce que vous demandez, ici c'est juste vous et moi et les commentaires ne sont pas appropriés pour poser des questions. N'oubliez pas de poster un lien vers une nouvelle question ici afin que je puisse suivre.
Tomasz Nurkiewicz
61

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' thisobjet de l'appel. Les méthodes synchronisées statiques verrouillent l' Classobjet lui-même.

De même, entrer un bloc synchronisé nécessite de verrouiller l' thisobjet 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é .

David R Tribble
la source
25

volatil:

volatileest un mot-clé. volatileforce 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 volatilevariables 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 volatilevariable sont toujours visibles pour les autres threads . De plus, cela signifie également que lorsqu'un thread lit une volatilevariable, 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:

AtomicXXXLes classes prennent en charge la programmation sans verrous et sans thread sur des variables uniques. Ces AtomicXXXclasses (comme AtomicInteger) 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é:

synchronizedest un mot clé utilisé pour protéger une méthode ou un bloc de code. La méthode synchronisée a deux effets:

  1. Premièrement, il n'est pas possible que deux invocations de synchronizedméthodes sur le même objet s'entrelacent. Lorsqu'un thread exécute une synchronizedméthode pour un objet, tous les autres threads qui appellent des synchronizedméthodes pour le même bloc d'objet (suspendre l'exécution) jusqu'à ce que le premier thread soit terminé avec l'objet.

  2. Deuxièmement, lorsqu'une synchronizedméthode se termine, elle établit automatiquement une relation passe-avant avec toute invocation ultérieure d'une synchronizedmé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

AtomicXXXest équivalent volatile + synchronizedmême si l'implémentation est différente. AmtomicXXXétend les volatilevariables + compareAndSetmé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

Ravindra babu
la source
2
Il s'agit de la première réponse qui mentionne en fait la sémantique qui se produit avant les mots-clés / fonctionnalités décrits, qui sont importants pour comprendre comment ils affectent réellement l'exécution du code. Les réponses votées plus élevées manquent cet aspect.
jhyot
5

Je sais que deux threads ne peuvent pas entrer dans le bloc Synchroniser 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 dans 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 se bloquer à chaque fois sur un objet différent.

si cela est vrai que Comment cette atomic.incrementAndGet () fonctionne sans Synchroniser ?? et est thread safe ??

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.

Et quelle est la différence entre la lecture interne et l'écriture dans une variable volatile / variable atomique ??

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.

j'ai lu dans un article que le thread a une copie locale des variables qu'est-ce que ??

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.

Peter Lawrey
la source
@Aniket Thakur en êtes-vous sûr? L'entier est immuable. Donc, i ++ décompactera probablement automatiquement la valeur int, l'incrémentera, puis créera un nouvel entier, qui n'est pas la même instance qu'auparavant. Essayez de rendre i final et vous obtiendrez des erreurs de compilation lors de l'appel à i ++.
fuemf5
2

Synchronized Vs Atomic Vs Volatile:

  • Volatile et Atomic s'applique uniquement sur la variable, tandis que Synchronized s'applique sur la méthode.
  • Volatile assure la visibilité et non l'atomicité / la cohérence de l'objet, tandis que d'autres garantissent la visibilité et l'atomicité.
  • Stock de variables volatiles dans la RAM et son accès est plus rapide, mais nous ne pouvons pas garantir la sécurité des threads ou la synchronisation sans mot-clé synchronisé.
  • Synchronisé implémenté comme bloc synchronisé ou méthode synchronisée alors que les deux ne le sont pas. Nous pouvons enfiler plusieurs lignes de code en toute sécurité à l'aide d'un mot clé synchronisé, tandis qu'avec les deux, nous ne pouvons pas obtenir la même chose.
  • Synchronisé peut verrouiller le même objet de classe ou un objet de classe différent alors que les deux ne le peuvent pas.

Veuillez me corriger si je manque quelque chose.

Ajay Gupta
la source
1

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.

Thomas Mathew
la source
1

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.

Sai prateek
la source