Il est important de comprendre qu'il existe deux aspects à la sécurité des threads.
- contrôle d'exécution, et
- 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 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 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 volatile
modifie également le traitement de ces derniers long
et l' double
exige 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 A
lorsqu'il écrit dans le champ volatile f
devient visible par le thread B
lors 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 volatile
est 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+write
instructions 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.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
la source
synchronized
est 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 lesynchronized
bloc. 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 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 auxvolatile
variables. Tous les threads peuvent accéder simultanément à une valeur de variable volatile.Un bon exemple pour utiliser une variable volatile:
Date
variable.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.Jetez un œil à cet article pour une meilleure compréhension du
volatile
concept.Lawrence Dol a clairement expliqué votre
read-write-update query
.Concernant vos autres requêtes
Vous devez utiliser
volatile
si 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.La réponse sera la même que dans la première requête.
Reportez-vous à cet article pour une meilleure compréhension.
la source
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
volatile
peut 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
x
initialement é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
synchronized
sur 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, six
é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.Marquer
x
commevolatile
ne rend pasx++;
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 dex
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 serafalse
.Le marquage d'une variable
volatile
indique fondamentalement à tous les threads d'effectuer des opérations de lecture et d'écriture sur la mémoire principale uniquement.synchronized
indique à 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:
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
b
sur true), car ilb
est défini sur true uniquement aprèsx
est défini sur 20, mais le compilateur / CPU peut décider de réorganiser threadA, dans ce cas, threadB peut également imprimer 10. Marquerb
commevolatile
garantit 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 variablevolatile
garantit é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.
la source
INC
opé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/DEC
Cependant , les commandes peuvent être marquées atomiquement dans l'assembly et être toujours une opération atomique.