Différence entre volatile et synchronisé en Java

Je me demande à la différence entre déclarer une variable comme 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 information:

  • l’access à une variable volatile n’a jamais le potentiel de bloquer: nous ne faisons qu’une simple lecture ou écriture, donc contrairement à un bloc synchronisé, nous ne conserverons jamais aucun verrou;
  • parce que l’access à une variable volatile ne contient jamais de verrou, cela ne convient pas dans les cas où nous voulons lire et mettre à jour l’écriture en tant qu’opération atomique (sauf si nous sums prêts à “manquer une mise à jour”);

Que veulent-ils dire par read-update-write ? N’est-ce pas une écriture aussi une mise à jour ou cela signifie-t-il simplement que la mise à jour est une écriture qui dépend de la lecture?

Surtout, quand est-il plus approprié 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 de l’entrée? Par exemple, il existe une variable appelée render qui est lue dans la boucle de rendu et définie par un événement de pression de touche?

Il est important de comprendre que la sécurité des threads comporte deux aspects.

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

Le premier a trait au contrôle du moment où le code s’exécute (y compris l’ordre dans lequel les instructions sont exécutées) et de son exécution simultanée, et le second quand les effets en mémoire de ce qui a été fait sont visibles pour les autres threads. Chaque processeur ayant plusieurs niveaux de cache entre lui et la mémoire principale, les threads exécutés sur des processeurs ou des cœurs différents peuvent voir la mémoire différemment à tout moment car les threads peuvent obtenir et travailler sur des copies privées de la mémoire principale.

L’utilisation de la synchronized empêche tout autre thread d’obtenir le moniteur (ou le verrou) pour le même object , empêchant ainsi l’exécution simultanée de tous les blocs de code protégés par la synchronisation sur le même object . La synchronisation crée également une barrière de mémoire “arrive avant”, provoquant une contrainte de visibilité de la mémoire telle que tout ce qui se produit jusqu’à ce qu’un thread libère un verrou apparaît à un autre thread acquérant ensuite le même verrou avant l’acquisition du 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 lors de sa libération, les deux étant relativement coûteux.

L’utilisation de volatile , d’un autre côté, force tous les access (en lecture ou en écriture) à la variable volatile à se produire dans la mémoire principale, ce qui empêche la variable volatile de se trouver 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 d’access ne soit pas important. Utiliser volatile change également le traitement de long et double pour exiger que les access à eux 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 de la volatilité a été renforcée pour être presque aussi forte que synchronisée 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 access à 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 access aux champs normaux 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 moniteur acquiert. En effet, du fait que le nouveau modèle de mémoire impose des contraintes plus ssortingctes sur la réorganisation des access aux champs volatiles avec les autres access aux champs, volatiles ou non, tout ce qui était visible au thread A sur le champ volatil f devient visible sur f .

– FAQ JSR 133 (Java Memory Model)

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 l’exécution d’exécuter de nouveau les instructions à travers la barrière. Dans l’ancien JMM, volatile n’a pas empêché la réorganisation. Cela peut être important car, à part les 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 exécutées exactement dans l’ordre dans lequel elles apparaissaient. la source.

Une utilisation de volatile est pour un object partagé mais immuable est recréé à la volée, avec beaucoup d’autres threads prenant une référence à l’object à un moment donné de leur cycle d’exécution. Il faut que les autres threads commencent à utiliser l’object recréé une fois qu’il est publié, mais qu’il n’a pas besoin de la surcharge supplémentaire de la synchronisation complète et de la contention 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 Ssortingng getError() { SomeObject myCopy=SharedLocation.someObject; // gets current copy ... int cod=myCopy.getErrorCode(); Ssortingng 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 parlant de votre question read-update-write, 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 entrer simultanément. Parmi les nombreuses permutations de ce qui pourrait arriver, l’une est que thread-1 fait le test pour le compteur == 1000 et le trouve vrai et est ensuite suspendu. Ensuite, thread-2 fait le même test et le voit également vrai et est suspendu. Puis le thread-1 reprend et met le compteur à 0. Puis le thread-2 reprend et met à nouveau 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 différentes en cache é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 à cause de la mise en cache.

Ce qui est important dans cet exemple, c’est que le compteur de variables était lu de la mémoire principale dans le cache, mis à jour dans le cache et réécrit dans la mémoire principale à un moment indéterminé lorsqu’une barrière de mémoire se produisait. Rendre le compteur volatile est insuffisant pour la sécurité des threads de ce code, car le test du maximum et des affectations sont des opérations discrètes, y compris l’incrément qui est un ensemble d’instructions de read+increment+write non-atomique.

 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 object entièrement formé est uniquement en lecture ou en écriture (et, en fait, il est généralement écrit à partir d’un seul point). Un autre exemple serait une référence à un tableau volatile dans une liste de copie sur écriture, à condition que le tableau ne soit lu qu’en prenant d’abord une copie locale de la référence.

volatile est un modificateur de champ , tandis que synchronisé 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 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 les mêmes que celles contenues dans les autres threads. En particulier, un autre thread peut avoir mis à jour i1 dans son thread, mais la valeur dans le thread en cours peut être différente de cette 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 pour les 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 , de 2 pour i1 pour thread1 et de 3 pour thread1 pour thread1 si thread1 et thread2 ont tous deux mis à jour i1 mais cette valeur mise à jour n’a pas encore été propagée à la mémoire “principale” ou à d’autres threads.

En revanche, geti2() accède effectivement à la valeur de i2 partir de la mémoire “principale”. Une variable volatile n’est pas autorisée à avoir une copie locale d’une variable différente de la valeur actuellement contenue 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 un thread, tous les autres threads voient immédiatement la même valeur. Les variables généralement volatiles ont un access plus élevé et une surcharge de mise à jour que les variables “simples”. Généralement, les threads peuvent avoir leur propre copie de données pour une meilleure efficacité.

Il y a deux différences entre la volatilité et la synchronisation.

Tout d’abord, synchronisé obtient et libère des verrous sur les moniteurs qui peuvent forcer un seul thread à la fois pour exécuter un bloc de code. C’est l’aspect assez connu de la synchronisation. Mais synchronisé synchronise également la mémoire. En fait, synchronisé synchronise la totalité de la mémoire de thread avec la mémoire “principale”. Ainsi, l’exécution de geti3() effectue les opérations suivantes:

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

Donc, lorsque volatile ne synchronise que la valeur d’une variable entre la mémoire de thread et la mémoire “principale”, synchronisée 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 surcharge que de volatilité.

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

synchronized est le modificateur de ressortingction d’access 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 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’access variable qui force tous les threads à récupérer la dernière valeur de la variable dans la mémoire principale. Aucun locking n’est requirejs 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 de Date .

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

entrer la description de l'image ici

Jetez un oeil à cet article pour mieux comprendre le concept de volatile .

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

En ce qui concerne vos autres requêtes

Quand est-il plus approprié de déclarer les variables volatiles que d’y accéder via une synchronisation?

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 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.

  1. volatile mot-clé volatile dans Java est un modificateur de champ, alors que synchronized modifie des blocs et des méthodes de code.

  2. synchronized obtient et libère le verrou sur le mot-clé java volatile moniteur ne nécessite pas cela.

  3. Les threads en Java peuvent être bloqués pour attendre n’importe quel moniteur en cas de synchronized , ce qui n’est pas le cas avec le mot-clé volatile en Java.

  4. synchronized méthode synchronized affecte les performances plus que le mot-clé volatile en Java.

  5. Puisque le mot-clé volatile en Java synchronise uniquement la valeur d’une variable entre la mémoire Thread et la mémoire “principale” alors que le mot-clé synchronisé synchronise la valeur de toutes les variables entre la mémoire et la mémoire principale et verrouille et libère un moniteur pour démarrer. Pour cette raison, le mot-clé synchronized en Java est susceptible d’avoir plus de surcharge que de volatile .

  6. Vous ne pouvez pas synchroniser sur un object nul, mais votre variable volatile dans Java pourrait être nulle.

  7. De Java 5 L’écriture dans un champ volatile a le même effet de mémoire qu’une version de moniteur, et la lecture d’un champ volatile a le même effet de mémoire qu’un moniteur acquiert

J’aime l’explication de jenkov

Visibilité d’objects partagés

Si deux threads ou plus partagent un object, sans utiliser correctement les déclarations volatiles ou la synchronisation , les mises à jour de l’object partagé effectuées par un thread peuvent ne pas être visibles par les autres threads.

Imaginez que l’object partagé soit initialement stocké dans la mémoire principale. Un thread s’exécutant sur l’unité centrale lit alors l’object partagé dans son cache CPU. Là, il apporte une modification à l’object partagé. Tant que le cache du processeur n’a pas été vidé dans la mémoire principale, la version modifiée de l’object partagé n’est pas visible pour les threads s’exécutant sur d’autres processeurs. De cette façon, chaque thread peut se retrouver avec sa propre copie de l’object partagé, chaque copie étant placée dans un cache de processeur différent.

Le diagramme suivant illustre la situation esquissée. Un thread s’exécutant sur le processeur de gauche copie l’object partagé dans son cache de processeur et modifie sa variable count sur 2. Ce changement n’est pas visible par les autres threads exécutés sur le bon processeur, car la mise à jour à compter n’a pas été renvoyée à main mémoire encore.

entrer la description de l'image ici

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

Conditions de course

Si deux threads ou plus partagent un object et que plusieurs threads mettent à jour des variables dans cet object partagé, des conditions de concurrence peuvent survenir.

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

Si ces incréments avaient été effectués séquentiellement, le nombre de variables serait incrémenté deux fois et la valeur d’origine + 2 serait réécrite dans la mémoire principale.

Cependant, les deux incréments ont été effectués simultanément sans synchronisation correcte. Indépendamment du thread A et B qui écrit sa version mise à jour du compte dans la mémoire principale, la valeur mise à jour ne sera supérieure que de 1 à la valeur d’origine, malgré les deux incréments.

Ce diagramme illustre une occurrence du problème des conditions de course décrit ci-dessus:

entrer la description de l'image ici

Pour résoudre ce problème, vous pouvez utiliser un bloc Java synchronisé . Un bloc synchronisé garantit qu’un seul thread peut entrer une section critique donnée du code à un moment donné. Les blocs synchronisés garantissent également que toutes les variables accédées dans le bloc synchronisé seront lues à partir de la mémoire principale et que lorsque le thread quitte le bloc synchronisé, toutes les variables mises à jour seront de nouveau enregistrées dans la mémoire principale. ne pas.