web-dev-qa-db-fra.com

Différence entre volatile et synchronisé en Java

Je me demande quelle est la différence entre déclarer une variable en tant que volatile et toujours accéder à la variable dans un bloc synchronized(this) 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 blocage: nous ne faisons qu'une simple lecture ou écriture, ce qui fait que, contrairement à un bloc synchronisé, nous ne conserverons jamais aucun verrou;
  • parce qu'accéder à une variable volatile ne contient jamais de verrou, cela ne convient pas dans les cas où nous voulons read-update-write sous forme d'opération atomique (à moins que nous ne soyons prêts à "manquer une mise à jour");

Que veulent-ils dire par read-update-write? Une écriture n'est-elle pas aussi une mise à jour ou signifient-elles simplement que le update est une écriture qui dépend de la lecture?

Surtout, à quel moment convient-il de déclarer les variables volatile plutôt que d'y accéder via un bloc synchronized? Est-ce une bonne idée d'utiliser volatile pour les variables qui dépendent des entrées? Par exemple, une variable appelée render est lue dans la boucle de rendu et définie par un événement de frappe?

194
Albus Dumbledore

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

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

Le premier concerne 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 lorsque les effets en mémoire de ce qui a été fait sont visibles par les autres threads. Chaque CPU ayant plusieurs niveaux de cache entre la mémoire principale et la mémoire principale, les threads s'exécutant sur différents CPU ou cœurs peuvent voir la "mémoire" différemment à tout moment, car ils sont autorisés à obtenir et à utiliser des copies privées de la mémoire principale.

Utiliser synchronized empê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 la synchronisation sur le même objet de s'exécuter simultanément. La synchronisation crée également une barrière de mémoire "se produit-avant", ce qui entraîne une contrainte de visibilité de la mémoire telle que tout ce qui est fait jusqu'au point où un thread libère un verrou apparaît. à un autre thread acquérant ensuite le même verrou avant qu'il ait acquis le fermer à clé. Concrètement, sur le matériel actuel, cela provoque généralement le vidage des caches de la CPU lors de l'acquisition d'un moniteur et son écriture dans la mémoire principale lors de sa libération, les deux étant (relativement) coûteux.

En revanche, l'utilisation de volatile force tous les accès (en lecture ou en écriture) à la variable volatile à se produire en mémoire principale, ce qui permet de conserver efficacement la variable volatile dans les caches du processeur. 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 de volatile modifie également le traitement de long et double pour exiger que les accès à celles-ci soient atomiques; sur certains matériels (plus anciens), cela peut nécessiter des verrous, mais pas sur du matériel 64 bits moderne. Dans le nouveau modèle de mémoire (JSR-133) pour Java 5+, la sémantique de volatile a été renforcée pour être presque aussi puissante que la synchronisation en ce qui concerne la visibilité de la mémoire et le classement des instructions (voir http : //www.cs.umd.edu/users/pugh/Java/memoryModel/jsr-133-faq.html#volatile ). Aux fins de la 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 les unes avec les autres. La différence est qu’il n’est plus aussi facile de réorganiser les accès normaux aux champs autour d’eux. L'écriture dans un champ volatile a le même effet de mémoire qu'une libération de moniteur, et la lecture d'un champ volatile a le même effet de mémoire qu'un disque acquis par un moniteur. En effet, comme le nouveau modèle de mémoire impose des contraintes plus strictes lors de la réorganisation des accès aux champs volatiles avec d'autres accès aux champs, volatiles ou non, tout ce qui était visible dans le thread A lorsqu'il écrit dans le champ volatile f devient visible. enfiler B quand il lit f.

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

Ainsi, à présent, 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 programme d'exécution de réorganiser les instructions à travers la barrière. Dans l’ancien JMM, volatile n’empêchait pas de procéder à une nouvelle commande. Cela peut être important, car en dehors des barrières de mémoire, la seule limitation imposée est que, pour un thread particulier , l'effet net du code est le même que si les instructions étaient affichées. ont été exécutés dans l’ordre précis où ils apparaissent dans la source.

L'une des utilisations de volatile est un objet partagé mais immuable qui est recréé à la volée, de nombreux autres threads prenant une référence à l'objet à un moment donné de leur cycle d'exécution. Il faut que les autres threads commencent à utiliser l’objet recréé une fois publié, mais n’ont pas besoin de la surcharge supplémentaire liée à la synchronisation complète, ni à la contention de celui-ci et à l’effacement 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.

En répondant à votre question lecture-mise à jour-écriture, en particulier. Considérez le code non sécurisé suivant:

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

Maintenant, avec la méthode updateCounter () non synchronisée, deux threads peuvent l’entrer en même temps. Parmi les nombreuses permutations possibles, on peut citer le thread-1 qui teste le compteur == 1000 et le trouve vrai, puis est suspendu. Ensuite, thread-2 fait le même test et le voit également comme vrai et est suspendu. Ensuite, thread-1 reprend et met le compteur à 0. Ensuite, le thread-2 reprend et met à nouveau le compteur à 0 car il a manqué la mise à jour de thread-1. Cela peut également se produire même si la commutation de threads ne se produit pas comme je l'ai décrit, mais simplement parce que deux copies de compteur mises 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 cœur séparé. D'ailleurs, un thread pourrait avoir un compteur à une valeur et l'autre pourrait avoir un compteur à une valeur totalement différente juste à cause de la mise en cache.

Ce qui est important dans cet exemple, c'est que la variable counter a été lue dans la mémoire principale dans le cache, mise à jour dans le cache et réécrite dans la mémoire principale à un moment indéterminé, plus tard, lorsqu'une barrière mémoire s'est produite. ou quand la mémoire cache était nécessaire pour autre chose. Faire le compteur volatile est insuffisant pour la sécurité du thread 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'instructions non atomiques read+increment+write, quelque chose comme:

MOV EAX,counter
INC EAX
MOV counter,EAX

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

357
Lawrence Dol

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 accesseur simple 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 dans i1 dans le thread en cours . Les threads peuvent avoir des copies locales de variables et les données ne doivent pas nécessairement être identiques à celles des autres threads. En particulier, un autre thread peut avoir mis à jour i1 dans son thread, mais la valeur du thread actuel peut être différente de cette valeur mise à jour. En fait, Java a l’idée d’une mémoire "principale", 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 de 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 i1 et que thread2 ait un valeur de 3 pour i1 si thread1 et thread2 ont tous deux été mis à jour, mais cette valeur n'a pas encore été propagée à la mémoire "principale" ou à d'autres threads.

Par ailleurs, geti2() accède effectivement à la valeur de i2 à partir de la mémoire "principale". Une variable volatile n'est pas autorisée à disposer d'une copie locale d'une variable 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 à la variable ou la mettez à jour dans un thread, tous les autres threads voient immédiatement la même valeur. Généralement, les variables volatiles ont un accès et une surcharge de mise à jour plus importants 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 volitile et synchronisé.

Premièrement, synchronized obtient et libère des verrous sur les moniteurs, ce qui ne peut 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 de thread avec la mémoire "principale". Donc, exécuter geti3() fait ce qui suit:

  1. Le thread acquiert le verrou sur le moniteur pour l'objet this.
  2. La mémoire de thread supprime toutes ses variables, c’est-à-dire qu’elle lit toutes ses variables dans la mémoire "principale".
  3. Le bloc de code est exécuté (dans ce cas, réglez la valeur de retour sur la valeur actuelle de i3, qui vient d’être réinitialisée à partir de la mémoire "principale").
  4. (Toute modification apportée aux variables devrait normalement être é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 l'objet this. 

Ainsi, où volatile ne synchronise que la valeur d'une variable entre la mémoire de thread et la mémoire "principale", synchronized synchronise la valeur de toutes les variables entre la mémoire de thread et la mémoire "principale", et verrouille et libère un moniteur pour démarrer. Clairement synchronisé est susceptible d'avoir plus de frais généraux que volatile. 

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

93
Kerem Baydoğan

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

volatile est un modificateur d'accès variable qui oblige tous les threads à obtenir la dernière valeur de la variable à partir de la mémoire principale. Aucun verrouillage n'est requis pour accéder aux variables volatile. Tous les threads peuvent accéder à la valeur de la variable volatile en même temps. 

Un bon exemple d'utilisation de la variable volatile: variable Date.

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

enter image description here

Jetez un œil à ce article pour une meilleure compréhension du concept volatile

Lawrence Dol cleary a 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 par le biais de synchronisé? 

Vous devez utiliser volatile si vous pensez que tous les threads doivent obtenir la valeur réelle de la variable en temps réel, comme dans 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 des entrées?

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

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

17
Ravindra babu

J'aime le jenkov's explication

Visibilité des objets partagés

Si plusieurs threads partagent un objet sans utiliser correctement les déclarations volatile ni synchronization, les mises à jour de l'objet partagé effectuées par un thread peuvent ne pas être visibles par les autres threads.

Imaginez que l'objet partagé soit initialement stocké dans la mémoire principale. Un thread en cours d'exécution sur le processeur 1 lit ensuite l'objet partagé dans son cache de processeur. Là, il modifie l'objet partagé. Tant que le cache de la CPU n'a pas été vidé dans la mémoire principale, la version modifiée de l'objet partagé n'est pas visible pour les threads s'exécutant sur d'autres CPU. De cette façon, chaque thread peut se retrouver avec sa propre copie de l'objet partagé, chaque copie se trouvant dans un cache de CPU différent.

Le diagramme suivant illustre la situation esquissée. Un thread en cours d'exécution sur la CPU de gauche copie l'objet partagé dans son cache de CPU et modifie sa variable count en 2. Cette modification n'est pas visible par les autres threads s'exécutant sur la bonne CPU, pas encore été ramené à la mémoire principale.

 enter image description here

Pour résoudre ce problème, vous pouvez utiliser le mot clé volatile de Java . Le mot clé volatile permet de s'assurer qu'une variable donnée est lue directement dans la mémoire principale et toujours écrite dans la mémoire principale lors de la mise à jour.

Conditions de course

Si plusieurs threads partagent un objet et que plusieurs threads mettent à jour les variables de cet objet partagé, des conditions de concurrence peuvent se produire.

Imaginez si le thread A lit le nombre de variables d'un objet partagé dans son cache CPU. Imaginez aussi que le thread B fasse la même chose, mais dans un cache différent de la CPU. Maintenant, le fil A ajoute un pour compter, et le fil B fait la même chose. Maintenant, var1 a été incrémenté deux fois, une fois dans chaque cache de la CPU.

Si ces incréments avaient été effectués de manière séquentielle, le compte de variables serait incrémenté deux fois et la valeur originale + 2 seraient réécrites dans la mémoire principale.

Cependant, les deux incréments ont été effectués simultanément sans synchronisation appropriée. Quels que soient les threads A et B qui écrivent sa version mise à jour du compte dans la mémoire principale, la valeur mise à jour ne sera supérieure de 1 à la valeur d'origine, malgré les deux incréments.

Ce diagramme illustre l'occurrence du problème avec les conditions de concurrence décrites ci-dessus:

 enter image description here

Pour résoudre ce problème, vous pouvez utiliser un bloc synchronisé Java . Un bloc synchronisé garantit qu'un seul thread peut entrer une section critique du code à un moment donné. Les blocs synchronisés garantissent également que toutes les variables accédées à l'intérieur du bloc synchronisé seront lues dans la mémoire principale. Lorsque l'unité d'exécution quittera le bloc synchronisé, toutes les variables mises à jour seront de nouveau vidées dans la mémoire principale, que la variable soit déclarée volatile ou non. ne pas.

2
yoAlex5

tl; dr :

Le multithreading pose trois problèmes principaux:

1) Conditions de course

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

3) Optimisations du Complier et du CPU

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

Elaboration :

1) Considérez ce code comme étant dangereux:

x++; 

Bien que cela puisse ressembler à une opération, il s’agit en réalité de 3: lire la valeur actuelle de x dans la mémoire, l’ajouter à 1 et la sauvegarder dans la mémoire. Si peu de threads essaient de le faire en même temps, le résultat de l'opération n'est pas défini. Si x était à l'origine 1, après 2 threads utilisant le code, il peut être 2 et 3, en fonction du thread terminé, quelle partie de l'opération avant que le contrôle ne soit transféré à l'autre thread. Ceci est une forme de condition de concurrence

Utiliser synchronized sur un bloc de code rend atomic -, ce qui signifie que les opérations se déroulent comme si les 3 opérations se produisaient en même temps, et qu'il n'y avait aucun moyen pour qu'un autre thread vienne au milieu et interfère. Donc, si x était 1 et que 2 threads essayent de préformer x++ nous savons à la fin, il sera égal à 3. Cela résout donc le problème de la condition de concurrence critique. 

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

Marquer x comme volatile ne rend pas x++; atomique, il ne résout donc pas ce problème.

2) En outre, les threads ont leur propre contexte - c’est-à-dire qu’ils peuvent mettre en cache des 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 cela sur un thread, x = 10;. Et un peu plus tard, dans un autre fil, x = 20;. La modification de la valeur x peut 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 fait de marquer une variable comme étant volatile indique en principe à tous les threads d'effectuer des opérations de lecture et d'écriture uniquement sur la mémoire principale. synchronized indique à chaque thread d'aller mettre à jour sa valeur à partir de la mémoire principale quand ils entrent dans le bloc et de vider le résultat dans la mémoire principale quand ils quittent le bloc. 

Notez que contrairement aux courses de données, la mémoire obsolète n’est pas si facile à (re) produire, car des bouffées de chaleur dans la mémoire principale se produisent de toute façon. 

3) Le compliant et la CPU peuvent (sans aucune forme de synchronisation entre les threads) traiter tout le code comme un seul thread. Cela signifie qu’il peut s’agir d’un code extrêmement significatif dans un aspect multithreading et le traiter comme s’il s’agissait d’un seul thread, là où ce n’était pas si significatif. Ainsi, il peut examiner un code et décider, dans un souci d'optimisation, de le réorganiser, voire même d'en supprimer certaines parties, s'il ne sait pas que ce code est conçu pour fonctionner sur plusieurs threads. 

Considérons le code suivant:

boolean b = false;
int x = 10;

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

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

On pourrait penser que threadB pourrait uniquement imprimer 20 (ou ne rien imprimer du tout si threadB if-check est exécuté avant de définir b sur true), car b est défini sur true uniquement après que x est défini sur 20, mais le compilateur/CPU peut décidez de réorganiser threadA, dans ce cas, threadB pourrait également imprimer 10. Le marquage b en tant que volatile garantit qu'il ne sera pas réorganisé (ni ignoré dans certains cas). Ce qui signifie que le fil B ne peut imprimer que 20 (ou rien du tout). Marquer les méthodes comme synchronisées aboutira au même résultat. Le fait de marquer aussi une variable comme étant une variable volatile garantit uniquement qu’elle ne sera pas réorganisée, mais tout ce qui est avant/après peut toujours l'être, de sorte que la synchronisation peut être plus adaptée dans certains scénarios.

Notez qu'avant le nouveau modèle de mémoire Java 5, volatile ne résolvait pas ce problème. 

1
David Refaeli