Différence entre volatile et synchronisé en Java

233

Je me demande la différence entre déclarer une variable volatileet toujours accéder à la variable dans un synchronized(this)bloc en Java?

Selon cet article http://www.javamex.com/tutorials/synchronization_volatile.shtml, il y a beaucoup à dire et il y a beaucoup de différences mais aussi quelques similitudes.

Je suis particulièrement intéressé par cette info:

...

  • l'accès à une variable volatile n'a jamais le potentiel de bloquer: nous ne faisons jamais qu'une simple lecture ou écriture, donc contrairement à un bloc synchronisé, nous ne nous accrocherons jamais à un verrou;
  • parce que l'accès à une variable volatile ne contient jamais de verrou, il ne convient pas aux cas où nous voulons lire-mettre à jour-écrire comme une opération atomique (sauf si nous sommes prêts à "manquer une mise à jour");

Que signifient-ils par lecture-mise à jour-écriture ? Une écriture n'est-elle pas également une mise à jour ou signifie-t-elle simplement que la mise à jour est une écriture qui dépend de la lecture?

Surtout, quand est-il plus approprié de déclarer des variables volatileplutôt que d'y accéder via un synchronizedbloc? Est-ce une bonne idée d'utiliser volatilepour les variables qui dépendent de l'entrée? Par exemple, il existe une variable appelée renderqui est lue dans la boucle de rendu et définie par un événement de pression de touche?

Albus Dumbledore
la source

Réponses:

383

Il est important de comprendre qu'il existe deux aspects à la sécurité des threads.

  1. contrôle d'exécution, et
  2. visibilité de la mémoire

Le premier a à voir avec le contrôle de l'exécution du code (y compris l'ordre dans lequel les instructions sont exécutées) et s'il peut s'exécuter simultanément, et le second avec le moment où les effets en mémoire de ce qui a été fait sont visibles pour les autres threads. Étant donné que chaque processeur a plusieurs niveaux de cache entre lui et la mémoire principale, les threads s'exécutant sur différents processeurs ou cœurs peuvent voir la «mémoire» différemment à un moment donné car les threads sont autorisés à obtenir et à travailler sur des copies privées de la mémoire principale.

L'utilisation synchronizedempêche tout autre thread d'obtenir le moniteur (ou le verrou) pour le même objet , empêchant ainsi tous les blocs de code protégés par synchronisation sur le même objet de s'exécuter simultanément. La synchronisation crée également une barrière de mémoire «qui se produit avant», provoquant une contrainte de visibilité de la mémoire telle que tout ce qui est fait jusqu'au moment où un thread libère un verrou apparaît à un autre thread qui acquiert ensuite le même verrou qui s'est produit avant qu'il n'ait acquis le verrou. En termes pratiques, sur le matériel actuel, cela provoque généralement le vidage des caches du processeur lorsqu'un moniteur est acquis et écrit dans la mémoire principale lorsqu'il est libéré, les deux étant (relativement) coûteux.

L'utilisation volatile, d'autre part, force tous les accès (lecture ou écriture) à la variable volatile à se produire dans la mémoire principale, gardant ainsi la variable volatile hors des caches CPU. Cela peut être utile pour certaines actions où il est simplement nécessaire que la visibilité de la variable soit correcte et que l'ordre des accès ne soit pas important. L'utilisation volatilemodifie également le traitement de ces derniers longet l' doubleexige que leurs accès soient atomiques; sur certains matériels (plus anciens), cela peut nécessiter des verrous, mais pas sur du matériel 64 bits moderne. Sous le nouveau modèle de mémoire (JSR-133) pour Java 5+, la sémantique des volatiles a été renforcée pour être presque aussi forte que synchronisée en ce qui concerne la visibilité de la mémoire et l'ordre des instructions (voir http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Pour des raisons de visibilité, chaque accès à un champ volatile agit comme une demi-synchronisation.

Dans le nouveau modèle de mémoire, il est toujours vrai que les variables volatiles ne peuvent pas être réorganisées entre elles. La différence est qu'il n'est plus si facile de réorganiser les accès normaux aux champs autour d'eux. L'écriture dans un champ volatil a le même effet de mémoire qu'une libération de moniteur, et la lecture à partir d'un champ volatil a le même effet de mémoire qu'un acquisition de moniteur. En effet, parce que le nouveau modèle de mémoire impose des contraintes plus strictes sur le réordonnancement des accès aux champs volatils avec d'autres accès aux champs, volatils ou non, tout ce qui était visible par le thread Alorsqu'il écrit dans le champ volatile fdevient visible par le thread Blors de la lecture f.

- FAQ JSR 133 (modèle de mémoire Java)

Ainsi, les deux formes de barrière de mémoire (sous le JMM actuel) provoquent une barrière de réorganisation des instructions qui empêche le compilateur ou le run-time de réorganiser les instructions à travers la barrière. Dans l'ancien JMM, volatile n'empêchait pas de réorganiser. Cela peut être important, car en dehors des barrières de mémoire, la seule limitation imposée est que, pour tout thread particulier , l'effet net du code est le même que si les instructions étaient exécutées dans l'ordre précis dans lequel elles apparaissent dans le la source.

Une utilisation de volatile est pour un objet partagé mais immuable est recréé à la volée, avec de nombreux autres threads prenant une référence à l'objet à un moment particulier de leur cycle d'exécution. L'un a besoin des autres threads pour commencer à utiliser l'objet recréé une fois qu'il est publié, mais n'a pas besoin de la surcharge supplémentaire de synchronisation complète et de ses conflits d'accompagnants et du vidage du cache.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
//       someObject will be internally consistent for xxx(), a subsequent 
//       call to yyy() might be inconsistent with xxx() if the object was 
//       replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
private String getError() {
    SomeObject myCopy=SharedLocation.someObject; // gets current copy
    ...
    int cod=myCopy.getErrorCode();
    String txt=myCopy.getErrorText();
    return (cod+" - "+txt);
    }
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

Répondre à votre question de lecture-mise à jour-écriture, en particulier. Considérez le code dangereux suivant:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

Maintenant, avec la méthode updateCounter () non synchronisée, deux threads peuvent y entrer en même temps. Parmi les nombreuses permutations de ce qui pourrait arriver, l'une est que le thread-1 fait le test du compteur == 1000 et le trouve vrai et est ensuite suspendu. Ensuite, le thread-2 fait le même test et le voit également vrai et est suspendu. Ensuite, le thread-1 reprend et met le compteur à 0. Puis le thread-2 reprend et remet le compteur à 0 car il a manqué la mise à jour du thread-1. Cela peut également se produire même si le changement de thread ne se produit pas comme je l'ai décrit, mais simplement parce que deux copies de compteur en cache différentes étaient présentes dans deux cœurs de processeur différents et que les threads s'exécutaient chacun sur un noyau distinct. D'ailleurs, un thread peut avoir un compteur à une valeur et l'autre peut avoir un compteur à une valeur entièrement différente simplement en raison de la mise en cache.

Ce qui est important dans cet exemple, c'est que le compteur de variables a été lu de la mémoire principale dans le cache, mis à jour dans le cache et seulement réécrit dans la mémoire principale à un moment indéterminé plus tard lorsqu'une barrière de mémoire s'est produite ou lorsque la mémoire cache était nécessaire pour autre chose. Faire le compteur volatileest insuffisant pour la sécurité des threads de ce code, car le test pour le maximum et les affectations sont des opérations discrètes, y compris l'incrément qui est un ensemble d' read+increment+writeinstructions machine non atomiques , quelque chose comme:

MOV EAX,counter
INC EAX
MOV counter,EAX

Les variables volatiles ne sont utiles que lorsque toutes les opérations qui y sont effectuées sont "atomiques", comme mon exemple où une référence à un objet entièrement formé est uniquement lue ou écrite (et, en fait, elle n'est généralement écrite qu'à partir d'un seul point). Un autre exemple serait une référence de tableau volatile soutenant une liste de copie sur écriture, à condition que le tableau ne soit lu qu'en y prenant d'abord une copie locale de la référence.

Lawrence Dol
la source
5
Merci beaucoup! L'exemple avec le compteur est simple à comprendre. Cependant, lorsque les choses deviennent réelles, c'est un peu différent.
Albus Dumbledore
"En termes pratiques, sur le matériel actuel, cela provoque généralement le vidage des caches du processeur lorsqu'un moniteur est acquis et écrit dans la mémoire principale lorsqu'il est libéré, les deux étant coûteux (relativement parlant)." . Lorsque vous dites caches CPU, est-ce la même chose que les piles Java locales à chaque thread? ou un thread a-t-il sa propre version locale de Heap? Excusez-moi si je suis idiot ici.
NishM
1
@nishm Ce n'est pas la même chose, mais cela inclurait les caches locaux des threads impliqués. .
Lawrence Dol
1
@ MarianPaździoch: Un incrément ou un décrément n'est PAS une lecture ou une écriture, c'est une lecture et une écriture; c'est une lecture dans un registre, puis un incrément de registre, puis une réécriture dans la mémoire. Les lectures et les écritures sont atomiques individuellement , mais plusieurs de ces opérations ne le sont pas.
Lawrence Dol
2
Ainsi, selon la FAQ, non seulement les actions effectuées depuis une acquisition de verrou sont rendues visibles après déverrouillage, mais toutes les actions effectuées par ce thread sont rendues visibles. Même actions effectuées avant l'acquisition du verrou.
Lii
97

volatile est un modificateur de champ , tandis que synchronized modifie les blocs de code et les méthodes . Nous pouvons donc spécifier trois variantes d'un simple accesseur en utilisant ces deux mots clés:

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()accède à la valeur actuellement stockée i1dans le thread actuel. Les threads peuvent avoir des copies locales de variables, et les données ne doivent pas nécessairement être les mêmes que celles contenues dans d'autres threads. En particulier, un autre thread peut avoir été mis i1à jour dans son thread, mais la valeur dans le thread actuel peut être différente de celle valeur mise à jour. En fait, Java a l'idée d'une mémoire "principale", et c'est la mémoire qui contient la valeur "correcte" actuelle des variables. Les threads peuvent avoir leur propre copie de données pour les variables, et la copie du thread peut être différente de la mémoire "principale". Donc en fait, il est possible que la mémoire "principale" ait une valeur de 1 pour i1, que thread1 ait une valeur de 2 pour i1et pour thread2avoir une valeur de 3 pour i1si thread1 et thread2 ont tous deux mis à jour i1 mais que cette valeur mise à jour n'a pas encore été propagée à la mémoire "principale" ou à d'autres threads.

D'un autre côté, geti2()accède efficacement à la valeur de la i2mémoire "principale". Une variable volatile n'est pas autorisée à avoir une copie locale d'une variable qui est différente de la valeur actuellement conservée dans la mémoire "principale". En effet, une variable déclarée volatile doit avoir ses données synchronisées sur tous les threads, de sorte que chaque fois que vous accédez ou mettez à jour la variable dans n'importe quel thread, tous les autres threads voient immédiatement la même valeur. En règle générale, les variables volatiles ont un coût d'accès et de mise à jour plus élevé que les variables "simples". Généralement, les threads sont autorisés à avoir leur propre copie de données pour une meilleure efficacité.

Il existe deux différences entre volatile et synchronisé.

Tout d'abord synchronisé obtient et libère des verrous sur les moniteurs qui ne peuvent forcer qu'un seul thread à la fois à exécuter un bloc de code. C'est l'aspect assez bien connu de la synchronisation. Mais synchronisé synchronise également la mémoire. En fait synchronisé synchronise l'ensemble de la mémoire des threads avec la mémoire "principale". Donc, l'exécution geti3()fait ce qui suit:

  1. Le thread acquiert le verrou sur le moniteur pour cet objet.
  2. La mémoire du thread vide toutes ses variables, c'est-à-dire qu'elle a toutes ses variables lues efficacement dans la mémoire "principale".
  3. Le bloc de code est exécuté (dans ce cas, définir la valeur de retour à la valeur actuelle de i3, qui peut juste avoir été réinitialisée à partir de la mémoire "principale").
  4. (Toute modification des variables serait normalement écrite dans la mémoire "principale", mais pour geti3 () nous n'avons aucune modification.)
  5. Le thread libère le verrou sur le moniteur pour cet objet.

Ainsi, lorsque volatile synchronise uniquement la valeur d'une variable entre la mémoire du thread et la mémoire "principale", synchronized synchronise la valeur de toutes les variables entre la mémoire du thread et la mémoire "principale", et verrouille et libère un moniteur pour démarrer. Clairement synchronisé aura probablement plus de frais généraux que volatile.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html

Kerem Baydoğan
la source
35
-1, Volatile n'acquiert pas de verrou, il utilise l'architecture CPU sous-jacente pour assurer la visibilité sur tous les threads après l'écriture.
Michael Barker
Il convient de noter qu'il peut y avoir des cas où un verrou peut être utilisé pour garantir l'atomicité des écritures. Par exemple, écrire un long sur une plate-forme 32 bits qui ne prend pas en charge les droits de largeur étendue. Intel évite cela en utilisant des registres SSE2 (128 bits de large) pour gérer les longs volatils. Cependant, considérer un volatile comme un verrou entraînera probablement des bugs désagréables dans votre code.
Michael Barker
2
La sémantique importante partagée par les verrous des variables volatiles est qu'ils fournissent tous les deux des bords Happens-Before (Java 1.5 et versions ultérieures). L'entrée dans un bloc synchronisé, la suppression d'un verrou et la lecture d'un volatile sont toutes considérées comme une "acquisition" et la libération d'un verrou, la sortie d'un bloc synchronisé et l'écriture d'un volatile sont toutes des formes de "libération".
Michael Barker
20

synchronizedest le modificateur de restriction d'accès au niveau méthode / niveau bloc. Il s'assurera qu'un thread possède le verrou pour la section critique. Seul le thread, qui possède un verrou, peut entrer dans le synchronizedbloc. Si d'autres threads tentent d'accéder à cette section critique, ils doivent attendre que le propriétaire actuel libère le verrou.

volatileest un modificateur d'accès variable qui force tous les threads à obtenir la dernière valeur de la variable de la mémoire principale. Aucun verrouillage n'est requis pour accéder aux volatilevariables. Tous les threads peuvent accéder simultanément à une valeur de variable volatile.

Un bon exemple pour utiliser une variable volatile: Datevariable.

Supposons que vous avez créé la variable Date volatile. Tous les threads qui accèdent à cette variable obtiennent toujours les dernières données de la mémoire principale afin que tous les threads affichent une valeur de date réelle (réelle). Vous n'avez pas besoin de différents threads affichant une heure différente pour la même variable. Tous les threads doivent afficher la bonne valeur de date.

entrez la description de l'image ici

Jetez un œil à cet article pour une meilleure compréhension du volatileconcept.

Lawrence Dol a clairement expliqué votre read-write-update query.

Concernant vos autres requêtes

Quand est-il plus approprié de déclarer des variables volatiles que d'y accéder via synchronisé?

Vous devez utiliser volatilesi vous pensez que tous les threads devraient obtenir la valeur réelle de la variable en temps réel comme l'exemple que j'ai expliqué pour la variable Date.

Est-ce une bonne idée d'utiliser volatile pour les variables qui dépendent de l'entrée?

La réponse sera la même que dans la première requête.

Reportez-vous à cet article pour une meilleure compréhension.

Ravindra babu
la source
Ainsi, la lecture peut avoir lieu en même temps, et tous les threads liront la dernière valeur car le processeur ne met pas en cache la mémoire principale dans le cache des threads du CPU, mais qu'en est-il de l'écriture? L'écriture ne doit pas être concurrente correcte? Deuxième question: si un bloc est synchronisé, mais que la variable n'est pas volatile, la valeur d'une variable dans un bloc synchronisé peut toujours être modifiée par un autre thread dans un autre bloc de code, n'est-ce pas?
the_prole
11

tl; dr :

Le multithreading comporte 3 problèmes principaux:

1) Conditions de course

2) Mise en cache / mémoire périmée

3) Optimisations Complier et CPU

volatilepeut résoudre 2 & 3, mais ne peut pas résoudre 1. synchronized/ les verrous explicites peuvent résoudre 1, 2 & 3.

Elaboration :

1) Considérez ce fil non sécurisé:

x++;

Bien que cela puisse ressembler à une opération, il s'agit en fait de 3: lire la valeur actuelle de x dans la mémoire, y ajouter 1 et la sauvegarder en mémoire. Si quelques threads essaient de le faire en même temps, le résultat de l'opération n'est pas défini. Si xinitialement était 1, après 2 threads utilisant le code, il peut être 2 et il peut être 3, selon le thread qui a terminé quelle partie de l'opération avant le transfert du contrôle à l'autre thread. Il s'agit d'une forme de condition de concurrence .

L'utilisation synchronizedsur un bloc de code le rend atomique - ce qui signifie qu'il fait comme si les 3 opérations se produisent en même temps, et il n'y a aucun moyen pour un autre thread de venir au milieu et d'interférer. Donc, si xétait 1, et 2 threads essaient de préformer, x++nous savons qu'à la fin, il sera égal à 3. Donc, cela résout le problème de la condition de concurrence.

synchronized (this) {
   x++; // no problem now
}

Marquer xcomme volatilene rend pas x++;atomique, donc cela ne résout pas ce problème.

2) De plus, les threads ont leur propre contexte - c'est-à-dire qu'ils peuvent mettre en cache les valeurs de la mémoire principale. Cela signifie que quelques threads peuvent avoir des copies d'une variable, mais ils opèrent sur leur copie de travail sans partager le nouvel état de la variable entre d'autres threads.

Considérez que sur un fil, x = 10;. Et un peu plus tard, dans un autre thread, x = 20;. La modification de la valeur de xpeut ne pas apparaître dans le premier thread, car l'autre thread a enregistré la nouvelle valeur dans sa mémoire de travail, mais ne l'a pas copiée dans la mémoire principale. Ou qu'il l'a copié dans la mémoire principale, mais le premier thread n'a pas mis à jour sa copie de travail. Donc, si maintenant le premier thread vérifie, if (x == 20)la réponse sera false.

Le marquage d'une variable volatileindique fondamentalement à tous les threads d'effectuer des opérations de lecture et d'écriture sur la mémoire principale uniquement. synchronizedindique à chaque thread d'aller mettre à jour leur valeur depuis la mémoire principale lorsqu'ils entrent dans le bloc, et de vider le résultat dans la mémoire principale lorsqu'ils quittent le bloc.

Notez que contrairement aux races de données, la mémoire obsolète n'est pas si facile à (re) produire, car les vidages de la mémoire principale se produisent de toute façon.

3) Le compliant et le CPU peuvent (sans aucune forme de synchronisation entre les threads) traiter tout le code comme un seul thread. Cela signifie qu'il peut regarder du code, qui est très significatif dans un aspect multithreading, et le traiter comme s'il s'agissait d'un seul thread, alors qu'il n'est pas si significatif. Il peut donc regarder un code et décider, dans un souci d'optimisation, de le réorganiser, voire d'en supprimer complètement certaines parties, s'il ne sait pas que ce code est conçu pour fonctionner sur plusieurs threads.

Considérez le code suivant:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

Vous pourriez penser que threadB ne pourrait imprimer que 20 (ou ne rien imprimer du tout si la vérification if de threadB est exécutée avant de définir bsur true), car il best défini sur true uniquement après xest défini sur 20, mais le compilateur / CPU peut décider de réorganiser threadA, dans ce cas, threadB peut également imprimer 10. Marquer bcomme volatilegarantit qu'il ne sera pas réorganisé (ou supprimé dans certains cas). Ce qui signifie que threadB ne pouvait imprimer que 20 (ou rien du tout). Marquer les méthodes comme synchronisées permettra d'obtenir le même résultat. Le marquage d'une variable volatilegarantit également qu'elle ne sera pas réorganisée, mais tout ce qui est avant / après peut toujours être réorganisé, de sorte que la synchronisation peut être plus adaptée dans certains scénarios.

Notez qu'avant Java 5 New Memory Model, volatile ne résolvait pas ce problème.

David Refaeli
la source
1
"Bien que cela puisse ressembler à une opération, il s'agit en fait de 3: lire la valeur actuelle de x dans la mémoire, y ajouter 1 et la sauvegarder en mémoire." - D'accord, car les valeurs de la mémoire doivent passer par les circuits du processeur pour être ajoutées / modifiées. Même si cela se transforme en une seule INCopération d' assemblage , les opérations sous-jacentes du processeur sont toujours 3 fois et nécessitent un verrouillage pour la sécurité des threads. Bon point. INC/DECCependant , les commandes peuvent être marquées atomiquement dans l'assembly et être toujours une opération atomique.
Zombies
@Zombies donc quand je crée un bloc synchronisé pour x ++, le transforme-t-il en INC / DEC atomique marqué ou utilise-t-il un verrou régulier?
David Refaeli
Je ne sais pas! Ce que je sais, c'est que les INC / DEC ne sont pas atomiques car pour un CPU, il doit charger la valeur et la LIRE et aussi L'ÉCRIRE (en mémoire), comme toute autre opération arithmétique.
Zombies