De nombreux amis ont peut-être entendu parler du mot-clé volatile et peuvent l'avoir utilisé. Avant Java 5, c'était un mot-clé controversé, car l'utiliser dans des programmes a souvent abouti à des résultats inattendus. Ce n'est qu'après que Java 5 a fait le mot clé volatil qui a retrouvé sa vitalité.
Bien que le mot-clé volatil soit littéralement simple à comprendre, il n'est pas facile de bien l'utiliser. Étant donné que le mot-clé volatil est lié au modèle de mémoire de Java, avant de le dire à la clé volatile, nous comprenons d'abord les concepts et les connaissances liés au modèle de mémoire, puis analysons le principe de mise en œuvre du mot-clé volatil et donnez enfin plusieurs scénarios d'utilisation du mot clé volatil.
Voici le plan du répertoire de cet article:
1. Concepts connexes de modèles de mémoire
Comme nous le savons tous, lorsqu'un ordinateur exécute un programme, chaque instruction est exécutée dans le CPU, et lors de l'exécution de l'instruction, elle impliquera inévitablement la lecture et l'écriture de données. Étant donné que les données temporaires pendant le fonctionnement du programme sont stockées dans la mémoire principale (mémoire physique), il y a un problème pour le moment. Étant donné que la vitesse d'exécution du CPU est très rapide, le processus de lecture des données de la mémoire et de l'écriture de données à la mémoire est beaucoup plus lent que l'exécution des instructions par le CPU. Par conséquent, si l'opération de données doit être effectuée par interaction avec la mémoire à tout moment, la vitesse de l'exécution de l'instruction sera considérablement réduite. Par conséquent, il y a un cache dans le CPU.
Autrement dit, lorsque le programme est en cours d'exécution, il copiera les données requises par l'opération de la mémoire principale au cache du CPU. Ensuite, lorsque le CPU effectue des calculs, il peut lire directement les données de son cache et y écrire des données. Une fois l'opération terminée, les données du cache seront rincées dans la mémoire principale. Donnons un exemple simple, comme le code suivant:
i = i + 1;
Lorsque le thread exécute cette instruction, il lira d'abord la valeur de I à partir de la mémoire principale, puis copiera une copie dans le cache, puis le CPU exécutera l'instruction pour ajouter 1 à I, puis écrivez les données sur le cache, et enfin actualiser la dernière valeur de I dans le cache à la mémoire principale.
Il n'y a aucun problème avec ce code en cours d'exécution dans un seul thread, mais il y aura des problèmes lors de l'exécution dans un multi-thread. Dans les CPU multi-core, chaque thread peut s'exécuter dans un CPU différent, donc chaque thread a son propre cache lors de l'exécution (pour les CPU monocomes, ce problème se produira réellement, mais il est exécuté séparément sous la forme de planification de thread). Dans cet article, nous prenons l'exemple du processeur multicœur.
Par exemple, deux threads exécutent ce code en même temps. Si la valeur de i est 0 au début, alors nous espérons que la valeur de I deviendra 2 après que les deux threads auront exécutés. Mais ce sera le cas?
Il peut y avoir l'une des situations suivantes: Au début, deux threads lisent la valeur de I et le stockent dans le cache de leurs processeurs respectifs, puis le thread 1 effectue une opération d'ajout 1, puis écrit la dernière valeur de I à la mémoire. À l'heure actuelle, la valeur de I dans le cache du thread 2 est toujours 0. Après avoir effectué l'opération 1, la valeur de i est 1, puis le thread 2 écrit la valeur de i à la mémoire.
La valeur du résultat final I est 1, pas 2. Il s'agit du célèbre problème de cohérence du cache. Cette variable accessible par plusieurs threads est généralement appelée variable partagée.
C'est-à-dire que si une variable est mise en cache dans plusieurs CPU (ne se produit généralement que pendant la programmation multithreading), il peut y avoir un problème d'incohérence du cache.
Afin de résoudre le problème d'incohérence du cache, il existe généralement deux solutions:
1) En ajoutant le verrouillage # verrouillage au bus
2) à travers le protocole de cohérence du cache
Ces deux méthodes sont fournies au niveau matériel.
Dans les premiers CPU, le problème de l'incohérence du cache a été résolu en ajoutant des verrous de verrouillage # au bus. Étant donné que la communication entre le processeur et d'autres composants est effectuée à travers le bus, si le bus est ajouté avec un verrouillage de verrouillage, cela signifie que d'autres CPU sont empêchés d'accéder à d'autres composants (comme la mémoire), afin qu'un seul CPU puisse utiliser la mémoire de cette variable. Par exemple, dans l'exemple ci-dessus, si un thread exécute I = I +1, et si le signal LCOK # LOCK est envoyé dans le bus pendant l'exécution de ce code, alors seulement après avoir attendu que le code soit entièrement exécuté, d'autres CPU peuvent lire la variable à partir de la mémoire où la variable I est localisée, puis effectuer les opérations correspondantes. Cela résout le problème de l'incohérence du cache.
Mais la méthode ci-dessus aura un problème, car d'autres CPU ne peuvent pas accéder à la mémoire pendant le verrouillage du bus, ce qui entraîne une inefficacité.
Ainsi, un protocole de cohérence du cache émerge. Le plus célèbre est le protocole d'Intel d'Intel, qui garantit que la copie des variables partagées utilisées dans chaque cache est cohérente. Son idée principale est: lorsque le CPU écrit des données, s'il constate que la variable qui est opérée est une variable partagée, c'est-à-dire qu'il existe une copie de la variable dans d'autres CPU, il signalera d'autres CPU pour définir la ligne de cache de la variable à un état invalide. Par conséquent, lorsque d'autres CPU ont besoin de lire cette variable et de constater que la ligne de cache qui cache la variable dans leur cache est invalide, elle reliera à partir de la mémoire.
2. Trois concepts en programmation simultanée
Dans la programmation simultanée, nous rencontrons généralement les trois problèmes suivants: problème d'atomicité, problème de visibilité et problème ordonné. Jetons un coup d'œil à ces trois concepts d'abord:
1. Atomicité
Atomicité: c'est-à-dire qu'une opération ou plusieurs opérations sont soit exécutées tout et le processus d'exécution ne sera interrompu par aucun facteur, soit il ne sera exécuté.
Un exemple très classique est le problème de transfert de compte bancaire:
Par exemple, si vous transférez 1 000 yuans du compte A au compte B, il inclura inévitablement 2 opérations: soustrayez 1 000 yuans du compte A et ajoutera 1 000 yuans au compte B.
Imaginez simplement quelles conséquences seront causées si ces deux opérations ne sont pas atomiques. Si 1 000 yuans sont soustraits du compte A, l'opération sera soudainement résiliée. Ensuite, 500 yuans ont été retirés de B, et après avoir retiré 500 yuans, alors le fonctionnement de l'ajout de 1 000 yuans au compte B. Cela conduira au fait que bien que le compte A ait moins 1 000 yuans, le compte B n'a pas reçu le 1 000 yuans transférés.
Par conséquent, ces deux opérations doivent être atomiques afin de s'assurer qu'il n'y a pas de problèmes inattendus.
Quels sont les résultats qui se refléteront dans la programmation simultanée?
Pour donner l'exemple le plus simple, pensez à ce qui se passerait si le processus d'attribution d'une variable 32 bits n'est pas atomique?
i = 9;
Si un thread exécute cette instruction, je suppose que l'attribution d'une variable 32 bits comprend deux processus: affectation d'un 16 bits inférieur et affectation d'un 16 bits plus élevé.
Ensuite, une situation peut se produire: lorsque la faible valeur 16 bits est écrite, elle est soudainement interrompue, et à ce moment un autre fil lit la valeur de I, alors ce qui est lu est la mauvaise données.
2. Visibilité
La visibilité fait référence au moment où plusieurs threads accèdent à la même variable, un thread modifie la valeur de la variable et d'autres threads peuvent immédiatement voir la valeur modifiée.
Pour un exemple simple, consultez le code suivant:
// Le code exécuté par le thread 1 est int i = 0; i = 10; // Le code exécuté par le thread 2 est j = i;
Si le thread d'exécution 1 est CPU1 et que le thread d'exécution 2 est CPU2. D'après l'analyse ci-dessus, nous pouvons voir que lorsque le thread 1 exécute la phrase I = 10, la valeur initiale de I sera chargée dans le cache de CPU1 puis attribué une valeur de 10. Ensuite, la valeur de I dans le cache de CPU1 devient 10, mais elle n'est pas immédiatement écrite à la mémoire principale.
À l'heure actuelle, Thread 2 exécute J = I, et il ira d'abord à la mémoire principale pour lire la valeur de I et le charger dans le cache de CPU2. Notez que la valeur de I dans la mémoire est toujours 0, donc la valeur de J sera 0, pas 10.
C'est le problème de visibilité. Une fois que le thread 1 modifie la variable I, le thread 2 ne voit pas immédiatement la valeur modifiée par le thread 1.
3. Ordre
Ordre: c'est-à-dire que l'ordre d'exécution des programmes est exécuté dans l'ordre du code. Pour un exemple simple, consultez le code suivant:
int i = 0; booléen drapeau = false; i = 1; // instruction 1 drapeau = true; // Déclaration 2
Le code ci-dessus définit une variable de type int, une variable de type booléen, puis attribue respectivement des valeurs aux deux variables. Du point de vue de la séquence de code, l'instruction 1 est avant l'instruction 2. Ainsi, lorsque le JVM exécutera réellement ce code, s'assurera-t-il que l'instruction 1 sera exécutée avant l'instruction 2? Pas nécessairement, pourquoi? La réorganisation des instructions peut se produire ici.
Expliquons ce qu'est la réorganisation des instructions. D'une manière générale, afin d'améliorer l'efficacité du fonctionnement du programme, le processeur peut optimiser le code d'entrée. Il ne garantit pas que l'ordre d'exécution de chaque instruction du programme est conforme à l'ordre dans le code, mais il garantira que le résultat d'exécution final du programme et le résultat de la séquence d'exécution du code sont cohérents.
Par exemple, dans le code ci-dessus, qui exécute l'instruction 1 et l'instruction 2 n'a d'abord aucun effet sur le résultat final du programme, il est possible que pendant le processus d'exécution, l'instruction 2 soit exécutée en premier et l'instruction 1 soit exécutée ultérieurement.
Mais sachez que bien que le processeur réorganire les instructions, il garantira que le résultat final du programme sera le même que la séquence d'exécution de code. Alors, quelles garantissent-elles? Jetons un coup d'œil à l'exemple suivant:
int a = 10; // instruction 1int r = 2; // instruction 2a = a + 3; // instruction 3r = a * a; // Déclaration 4
Ce code contient 4 instructions, donc une possible ordonnance d'exécution est:
Il est donc possible d'être l'ordonnance d'exécution: Énoncé 2 Instruction 1 Énoncé 4 Énoncé 3
Ce n'est pas possible car le processeur examinera la dépendance aux données entre les instructions lors de la réorganisation. Si une instruction d'instruction 2 doit utiliser le résultat de l'instruction 1, le processeur s'assurera que l'instruction 1 sera exécutée avant l'instruction 2.
Bien que la réorganisation n'affectera pas les résultats de l'exécution du programme dans un seul thread, qu'en est-il du multithreading? Voyons un exemple ci-dessous:
// Thread 1: context = loadContext (); // état 1Inited = true; // État 2 // Thread 2: while (! Inited) {sleep ()} dosomething withconfig (context);Dans le code ci-dessus, comme les instructions 1 et 2 n'ont pas de dépendances de données, elles peuvent être réorganisées. Si la réorganisation se produit, l'instruction 2 est d'abord exécutée lors de l'exécution du thread 1, et il s'agit du thread 2 pensera que le travail d'initialisation a été terminé, puis il sautera de la boucle while pour exécuter la méthode Dosomething withConfig (context). Pour le moment, le contexte n'est pas initialisé, ce qui entraînera une erreur de programme.
Comme on peut le voir à partir de ce qui précède, la réorganisation des instructions n'affectera pas l'exécution d'un seul thread, mais affectera l'exactitude de l'exécution simultanée des threads.
En d'autres termes, afin d'exécuter correctement les programmes simultanés, l'atomicité, la visibilité et l'ordre doivent être assurés. Tant que l'on n'est pas garanti, cela peut entraîner une exécution incorrecte du programme.
3. modèle de mémoire java
J'ai parlé de certains problèmes qui peuvent survenir dans les modèles de mémoire et la programmation simultanée. Jetons un coup d'œil au modèle de mémoire Java et étudions ce qui garantit que le modèle de mémoire Java nous fournit et quelles méthodes et mécanismes sont fournis en Java pour assurer l'exactitude de l'exécution du programme lors de l'exécution de la programmation multithread.
Dans la spécification de la machine virtuelle Java, il est tenté de définir un modèle de mémoire Java (JMM) pour bloquer les différences d'accès à la mémoire entre diverses plates-formes matérielles et les systèmes d'exploitation, afin de permettre aux programmes Java d'obtenir des effets d'accès à la mémoire cohérents sur diverses plates-formes. Alors, que le modèle de mémoire Java stipule? Il définit les règles d'accès pour les variables d'un programme. Pour le dire plus largement, il définit l'ordre d'exécution du programme. Notez que pour obtenir de meilleures performances d'exécution, le modèle de mémoire Java ne permet pas au moteur d'exécution d'utiliser les registres ou les caches du processeur pour améliorer la vitesse d'exécution des instructions, et il ne restreint pas le compilateur pour réorganiser les instructions. En d'autres termes, dans le modèle de mémoire Java, il y aura également des problèmes de cohérence du cache et des problèmes de réorganisation des instructions.
Le modèle de mémoire Java stipule que toutes les variables sont dans la mémoire principale (similaire à la mémoire physique mentionnée ci-dessus), et chaque thread a sa propre mémoire de travail (similaire au cache précédent). Toutes les opérations d'un thread sur une variable doivent être effectuées en mémoire de travail et ne peuvent pas fonctionner directement sur la mémoire principale. Et chaque thread ne peut pas accéder à la mémoire de travail des autres threads.
Pour donner un exemple simple: dans Java, exécutez l'énoncé suivant:
i = 10;
Le thread d'exécution doit d'abord attribuer la ligne de cache où la variable I est située dans son propre thread de travail, puis l'écrire dans la mémoire principale. Au lieu d'écrire directement la valeur 10 dans la mémoire principale.
Alors, quelles garanties le langage Java lui-même assure-t-il pour l'atomicité, la visibilité et l'ordre?
1. Atomicité
En Java, les opérations de lecture et d'affectation des variables des types de données de base sont des opérations atomiques, c'est-à-dire que ces opérations ne peuvent pas être interrompues et exécutées ou non.
Bien que la phrase ci-dessus semble simple, elle n'est pas si facile à comprendre. Voir l'exemple suivant I:
Veuillez analyser lequel des opérations suivantes sont des opérations atomiques:
x = 10; // déclaration 1y = x; // instruction 2x ++; // instruction 3x = x + 1; // Déclaration 4
À première vue, certains amis peuvent dire que les opérations dans les quatre déclarations ci-dessus sont toutes des opérations atomiques. En fait, la seule déclaration 1 est une opération atomique, et aucune des trois autres déclarations n'est des opérations atomiques.
L'énoncé 1 attribue directement la valeur 10 à x, ce qui signifie que le thread exécute cette instruction et écrit la valeur 10 directement dans la mémoire de travail.
L'énoncé 2 contient en fait 2 opérations. Il doit d'abord lire la valeur de x, puis écrire la valeur de x à la mémoire de travail. Bien que les deux opérations de lecture de la valeur de X et d'écriture de la valeur de X à la mémoire de travail soient des opérations atomiques, ce ne sont pas des opérations atomiques ensemble.
De même, x ++ et x = x + 1 incluent 3 opérations: lire la valeur de x, effectuer le fonctionnement de l'ajout de 1 et écrire la nouvelle valeur.
Par conséquent, seul le fonctionnement de l'énoncé 1 dans les quatre énoncés ci-dessus est atomique.
En d'autres termes, seule une lecture et une affectation simples (et le nombre doit être attribué à une variable, et l'affectation mutuelle entre les variables n'est pas une opération atomique) est une opération atomique.
Cependant, il y a une chose à noter ici: sous la plate-forme 32 bits, la lecture et l'attribution des données 64 bits doivent être effectuées via deux opérations, et son atomicité ne peut être garantie. Cependant, il semble que dans le dernier JDK, le JVM a assuré que la lecture et l'attribution de données 64 bits soient également un fonctionnement atomique.
À partir de ce qui précède, on peut voir que le modèle de mémoire Java garantit que les lectures et les affectations de base sont des opérations atomiques. Si vous souhaitez réaliser l'atomicité d'une plus grande gamme d'opérations, elle peut être réalisée par synchronisé et verrouiller. Étant donné que la synchronisation et le verrouillage peuvent garantir qu'un seul thread exécute le bloc de code à tout moment, il n'y aura naturellement pas de problème d'atomicité, assurant ainsi l'atomicité.
2. Visibilité
Pour la visibilité, Java fournit le mot-clé volatil pour assurer la visibilité.
Lorsqu'une variable partagée est modifiée par volatile, elle garantit que la valeur modifiée sera mise à jour immédiatement dans la mémoire principale, et lorsque d'autres threads doivent le lire, il lira la nouvelle valeur en mémoire.
Cependant, les variables partagées ordinaires ne peuvent garantir la visibilité, car il est incertain lorsque la variable partagée normale est écrite dans la mémoire principale après sa modification. Lorsque d'autres fils le lisent, l'ancienne valeur d'origine peut toujours être dans la mémoire, donc la visibilité ne peut être garantie.
De plus, le synchronisé et le verrouillage peuvent également garantir la visibilité. Synchronisé et Lock peuvent garantir qu'un seul thread acquiert le verrou en même temps et exécute le code de synchronisation. Avant de libérer le verrouillage, la modification de la variable sera actualisée à la mémoire principale. Par conséquent, la visibilité peut être garantie.
3. Ordre
Dans le modèle de mémoire Java, les compilateurs et les processeurs sont autorisés à réorganiser les instructions, mais le processus de réorganisation n'affectera pas l'exécution des programmes unique, mais affectera l'exactitude de l'exécution simultanée multi-thread.
En Java, une certaine "ligne d'ordre" peut être assurée par le mot clé volatil (le principe spécifique est expliqué dans la section suivante). De plus, le synchronisé et le verrouillage peuvent être utilisés pour assurer la commande. De toute évidence, le synchronisé et le verrouillage garantissent qu'il existe un thread qui exécute le code de synchronisation à chaque instant, ce qui équivaut à laisser les threads exécuter le code de synchronisation en séquence, ce qui garantit naturellement l'ordre.
De plus, le modèle de mémoire Java a une "ligne d'ordre" innée, c'est-à-dire qu'elle peut être garantie sans aucun moyen, ce qui est généralement appelé principe en passant. Si l'ordre d'exécution de deux opérations ne peut pas être dérivé du principe des événements, alors ils ne peuvent garantir leur ordre et que les machines virtuelles peuvent les réorganiser à volonté.
Présentez le principe en cours (principe de l'occurrence prioritaire):
Ces 8 principes sont extraits de "une compréhension approfondie des machines virtuelles Java".
Parmi ces 8 règles, les 4 premières règles sont plus importantes, tandis que les 4 dernières règles sont toutes évidentes.
Expliquons les 4 premières règles ci-dessous:
Pour les règles d'ordre du programme, je comprends que l'exécution d'un code de programme semble être commandée dans un seul thread. Notez que bien que cette règle mentionne que "l'opération écrite dans le front se produit d'abord dans l'opération écrite dans le dos", il devrait s'agir de l'ordre dans lequel le programme semble être exécuté dans la séquence de code, car la machine virtuelle peut réorganiser le code du programme instruit. Bien que la réorganisation soit effectuée, le résultat de l'exécution finale est cohérent avec l'exécution séquentielle du programme et ne commandera que des instructions qui n'ont pas de dépendances de données. Par conséquent, dans un seul thread, l'exécution du programme semble être exécutée de manière ordonnée, qui doit être comprise avec soin. En fait, cette règle est utilisée pour garantir l'exactitude des résultats d'exécution du programme dans un seul thread, mais il ne peut garantir l'exactitude du programme de manière multi-thread.
La deuxième règle est également plus facile à comprendre.
La troisième règle est une règle relativement importante et est également ce qui sera discuté plus tard. Intuitivement, si un thread écrit d'abord une variable, puis un thread se lit, l'opération d'écriture se produira certainement d'abord dans l'opération de lecture.
La quatrième règle reflète en fait que le principe en cours est transitif.
4. Analyse approfondie des mots clés volatils
J'ai déjà parlé de beaucoup de choses, mais elles ouvrent en fait la façon de dire le mot-clé volatil, alors passons au sujet.
1. Sémantique à deux couches de mots clés volatils
Une fois qu'une variable partagée (variables des membres de la classe, les variables de membres statiques de classe) est modifiée par volatile, il a deux couches de sémantique:
1) Assurer la visibilité des différents threads lors du fonctionnement de cette variable, c'est-à-dire qu'un thread modifie la valeur d'une certaine variable, et cette nouvelle valeur est immédiatement visible pour d'autres threads.
2) Il est interdit de réorganiser les instructions.
Regardons d'abord un morceau de code. Si le thread 1 est exécuté en premier et que le thread 2 est exécuté ultérieurement:
// Thread 1boolean stop = false; while (! stop) {DoSomething ();} // Thread 2Stop = true;Ce code est un morceau de code très typique, et de nombreuses personnes peuvent utiliser cette méthode de balisage lors de l'interruption de threads. Mais en fait, ce code fonctionnera-t-il complètement correctement? Le fil sera-t-il interrompu? Pas nécessairement. Peut-être que la plupart du temps, ce code peut interrompre les threads, mais il peut également ne pas interrompre le fil (bien que cette possibilité soit très petite, une fois que cela se produira, il provoquera une boucle morte).
Expliquons pourquoi ce code peut entraîner l'interruption du thread. Comme expliqué précédemment, chaque thread a sa propre mémoire de travail pendant le fonctionnement, donc lorsque le thread 1 est en cours d'exécution, il copiera la valeur de la variable d'arrêt et la mettra dans sa propre mémoire de travail.
Ensuite, lorsque le thread 2 modifie la valeur de la variable d'arrêt, mais n'a pas eu le temps de l'écrire dans la mémoire principale, le thread 2 va faire d'autres choses, puis le thread 1 ne connaît pas les modifications du thread 2 dans la variable d'arrêt, il continuera donc à boucler.
Mais après avoir modifié avec volatile, il devient différent:
Premièrement: l'utilisation du mot clé volatil forcera immédiatement la valeur modifiée à la mémoire principale;
Deuxièmement: si vous utilisez le mot clé volatil, lorsque le thread 2 le modifie, la ligne de cache de l'arrêt de la variable de cache dans la mémoire de travail du thread 1 sera invalide (si elle est reflétée dans la couche matérielle, la ligne de cache correspondante dans le cache L1 ou L2 du CPU est invalide);
Troisièmement: Étant donné que la ligne de cache de la variable de cache s'arrête dans la mémoire de travail du thread 1 n'est pas valide, le thread 1 le lira dans la mémoire principale lorsqu'il lit à nouveau la valeur de l'arrêt de la variable.
Ensuite, lorsque le thread 2 modifie la valeur d'arrêt (bien sûr, il y a 2 opérations ici, modifiant la valeur dans la mémoire de travail du thread 2, puis en écrivant la valeur modifiée à la mémoire), la ligne de cache de l'arrêt de la variable de cache dans la mémoire de travail du thread 1 sera invalide. Lorsque le fil 1 se lit, il constate que sa ligne de cache n'est pas valide. Il attendra que l'adresse de mémoire principale correspondante de la ligne de cache soit mise à jour, puis lira la dernière valeur dans la mémoire principale correspondante.
Alors ce que le thread 1 lit est la dernière valeur correcte.
2. Volatile garantit-il l'atomicité?
À partir de ce qui précède, nous savons que le mot-clé volatil garantit la visibilité des opérations, mais peut-il s'assurer que les opérations sur les variables sont atomiques?
Voyons un exemple ci-dessous:
Test de classe publique {public volatile int inc = 0; public void augmentation () {inc ++; } public static void main (String [] args) {final test test = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.inCrease (); }; }.commencer(); } while (thread.activeCount ()> 1) // Assurez-vous que les threads précédents ont terminé thread.yield (); System.out.println (test.inc); }}Pensez à ce qu'est le résultat de la sortie de ce programme? Peut-être que certains amis pensent que c'est 10 000. Mais en fait, l'exécution constatera que les résultats de chaque course sont incohérents, et c'est un nombre inférieur à 10 000.
Certains amis peuvent avoir des questions, c'est mal. Ce qui précède consiste à effectuer un opération d'auto-incrémentation sur la variable inc. Étant donné que Volatile garantit la visibilité, après l'auto-incrémentation de Inc dans chaque thread, la valeur modifiée peut être observée dans d'autres threads. Par conséquent, 10 threads ont effectué 1000 opérations respectivement, de sorte que la valeur finale de Inc devrait être de 1000 * 10 = 10000.
Il y a un malentendu ici. Le mot-clé volatil peut assurer la visibilité, mais le programme ci-dessus est mauvais car il ne peut garantir l'atomicité. La visibilité ne peut que garantir que la dernière valeur est lue à chaque fois, mais volatile ne peut garantir l'atomicité du fonctionnement des variables.
Comme mentionné précédemment, l'opération d'automobile n'est pas atomique. Il comprend la lecture de la valeur d'origine d'une variable, l'exécution d'une opération supplémentaire et l'écriture dans la mémoire de travail. C'est-à-dire que les trois sous-opérations de l'opération d'auto-incitation peuvent être effectuées séparément, ce qui peut conduire à la situation suivante:
Si la valeur de Variable Inc à un certain moment est de 10,
Le thread 1 effectue un fonctionnement d'auto-incidence sur la variable. Le thread 1 lit d'abord la valeur d'origine de la variable Inc, puis le thread 1 est bloqué;
Puis Thread 2 effectue un fonctionnement d'auto-incrémentation sur la variable, et Thread 2 lit également la valeur d'origine de la variable Inc. Étant donné que le thread 1 effectue uniquement une opération de lecture sur la variable Inc et ne modifie pas la variable, elle ne provoquera pas la ligne de cache de Cache Inc Cache Variable Inc dans le thread 2 n'est pas valide. Par conséquent, le thread 2 ira directement à la mémoire principale pour lire la valeur de Inc. Lorsqu'il est constaté que la valeur de Inc est 10, alors effectue une opération d'ajout 1 et écrit 11 à la mémoire de travail, et l'écrit enfin à la mémoire principale.
Ensuite, le thread 1 effectue ensuite l'opération d'addition. Étant donné que la valeur de l'inc a été lue, notez que la valeur de Inc dans le thread 1 est toujours 10 pour le moment, donc après le thread 1 ajoute Inc, la valeur de Inc est 11, puis écrit 11 pour travailler la mémoire et l'écrit enfin dans la mémoire principale.
Ensuite, après que les deux threads effectuent une opération d'auto-incrément, INC n'augmente que 1.
Ayant expliqué cela, certains amis peuvent avoir des questions, c'est mal. N'est-il pas garanti qu'une variable invalidera la ligne de cache lors de la modification de la variable volatile? Ensuite, d'autres threads liront la nouvelle valeur. Oui, c'est correct. Il s'agit de la règle de variable volatile dans la règle de se produire ci-dessus, mais il convient de noter que si le thread 1 lit la variable et est bloqué, la valeur inc ne sera pas modifiée. Ensuite, bien que volatile puisse garantir que le thread 2 lit la valeur de la variable Inc à partir de la mémoire, le thread 1 ne l'a pas modifié, donc le thread 2 ne verra pas du tout la valeur modifiée.
La cause profonde est que l'opération d'auto-inférences n'est pas une opération atomique et que volatile ne peut garantir qu'une opération sur les variables est atomique.
Modifier le code ci-dessus en l'un des éléments suivants peut réaliser l'effet:
Utiliser synchronisé:
Test de classe publique {public int inc = 0; Public synchronisé vide augmentation () {inc ++; } public static void main (String [] args) {final test test = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.inCrease (); }; }.commencer(); } while (thread.activeCount ()> 1) // Assurez-vous que les threads précédents ont terminé thread.yield (); System.out.println (test.inc); }} Utilisation de verrouillage:
Test de classe publique {public int inc = 0; Verrouillage de verrouillage = new reentrantLock (); public void augmentation () {lock.lock (); essayez {inc ++; } enfin {lock.unlock (); }} public static void main (string [] args) {final test test = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.inCrease (); }; }.commencer(); } while (thread.activeCount ()> 1) // Assurez-vous que les threads précédents ont été exécutés thread.yield (); System.out.println (test.inc); }} Utilisation d'atomicInteger:
Test de classe publique {public atomicInteger Inc = new atomicInteger (); public void augmentation () {inc.getAndInCment (); } public static void main (String [] args) {final test test = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.inCrease (); }; }.commencer(); } while (thread.activeCount ()> 1) // Assurez-vous que les threads précédents ont été exécutés thread.yield (); System.out.println (test.inc); }}Certaines classes d'opération atomique sont fournies sous le package java.util.concurrent.atomique de Java 1.5, à savoir l'auto-incrément (ajouter 1 opération), l'auto-décoricité (ajouter 1 opération), l'opération d'addition (ajouter un nombre) et l'opération de soustraction (ajouter un nombre) de types de données de base pour garantir que ces opérations sont des opérations atomiques. Atomic utilise CAS pour mettre en œuvre les opérations atomiques (comparer et échanger). CAS est en fait implémenté à l'aide des instructions CMPXCHG fournies par le processeur, et le processeur exécute les instructions CMPXCHG est une opération atomique.
3.Part volatile assurer l'ordre?
Comme mentionné précédemment, le mot-clé volatil peut interdire la réorganisation des instructions, de sorte que volatile peut garantir l'ordre dans une certaine mesure.
Il y a deux significations interdites de réorganisation des mots clés volatils:
1) Lorsque le programme exécute une opération de lecture ou d'écriture de la variable volatile, toutes les modifications des opérations précédentes doivent avoir été apportées et le résultat est déjà visible pour les opérations suivantes; Les opérations suivantes n'ont pas encore été effectuées;
2) Lors de l'optimisation de l'instruction, l'instruction accessible à la variable volatile ne peut pas être placée derrière elle, et les instructions suivant la variable volatile ne peuvent pas être placées devant elle.
Peut-être que ce qui est dit ci-dessus est un peu déroutant, alors donnez un exemple simple:
// x et y sont des variables non volatiles // Le drapeau est variable volatile x = 2; // instruction 1y = 0; // instruction 2Flag = true; // instruction 3x = 4; // déclaration 4y = -1; // Déclaration 5
Étant donné que la variable de drapeau est une variable volatile, lors de l'exécution du processus de réorganisation des instructions, l'énoncé 3 ne sera pas placé avant les déclarations 1 et 2, ni ne sera placé après l'énoncé 3 et les déclarations 4 et 5. Cependant, il n'est pas garanti que l'ordre des déclarations 1 et 2 et l'ordre de l'énoncé 4 et l'énoncé 5 ne sont pas garantis.
De plus, le mot-clé volatil peut garantir que lorsque l'instruction 3 est exécutée, l'énoncé 1 et l'instruction 2 doivent être exécutés et que les résultats d'exécution de l'énoncé 1 et de l'énoncé 2 sont visibles par l'énoncé 3, l'énoncé 4 et l'énoncé 5.
Revenons donc à l'exemple précédent:
// Thread 1: context = loadContext (); // état 1Inited = true; // État 2 // Thread 2: while (! Inited) {sleep ()} dosomething withconfig (context);Lorsque j'ai donné cet exemple, j'ai mentionné qu'il est possible que l'énoncé 2 soit exécuté avant l'énoncé 1, si longtemps il peut entraîner l'initialisation du contexte, et Thread 2 utilise le contexte non initialisé pour fonctionner, ce qui entraîne une erreur de programme.
Si la variable INIT est modifiée avec le mot clé volatil, ce problème ne se produira pas, car lorsque l'instruction 2 est exécutée, elle garantira certainement que le contexte a été initialisé.
4. Le principe et le mécanisme de mise en œuvre de la volatile
La description précédente de certaines utilisations du mot-clé volatil est originaire de. Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}Références:
《Java编程思想》
《深入理解Java虚拟机》
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。