De nombreux documents en ligne décrivent le modèle de mémoire Java, qui présentera qu'il existe une mémoire principale, et chaque thread de travailleur a sa propre mémoire de travail. Il y aura une seule pièce de données dans la mémoire principale et une pièce dans la mémoire de travail. Il y aura diverses opérations atomiques entre la mémoire de travail et la mémoire principale à synchroniser.
L'image suivante est de ce blog
Cependant, en raison de l'évolution continue de la version Java, le modèle de mémoire a également changé. Cet article ne parle que de certaines fonctionnalités du modèle de mémoire Java. Qu'il s'agisse d'un nouveau modèle de mémoire ou d'un ancien modèle de mémoire, il sera plus clair après avoir compris ces fonctionnalités.
1. Atomicité
L'atomicité signifie qu'une opération est sans interruption. Même lorsque plusieurs threads sont exécutés ensemble, une fois une opération démarrée, elle ne sera pas perturbée par d'autres threads.
On pense généralement que les instructions du CPU sont des opérations atomiques, mais le code que nous écrivons n'est pas nécessairement des opérations atomiques.
Par exemple, i ++. Cette opération n'est pas une opération atomique, elle est essentiellement divisée en 3 opérations, lire I, effectuer +1 et attribuer de la valeur à i.
Supposons qu'il y ait deux fils. Lorsque le premier thread lit i = 1, l'opération +1 n'a pas encore été effectuée et passez au deuxième thread. À ce moment, le deuxième fil lit également i = 1. Ensuite, les deux threads effectuent des opérations +1 ultérieures, puis attribuent des valeurs en arrière, I n'est pas 3, mais 2. Évidemment, il y a une incohérence dans les données.
Par exemple, la lecture d'une valeur longue 64 bits sur un JVM 32 bits n'est pas une opération atomique. Bien sûr, JVM 32 bits lit des entiers 32 bits comme une opération atomique.
2. Ordre
Au cours de la concurrence, l'exécution du programme peut être en panne.
Lorsqu'un ordinateur exécute du code, il ne s'exécute pas nécessairement dans l'ordre du programme.
classe OrdreRexample {int a = 0; booléen drapeau = false; public void writer () {a = 1; Flag = true; } public void Reader () {if (flag) {int i = a +1; }}} Par exemple, dans le code ci-dessus, deux méthodes sont appelées respectivement par deux threads. Selon Common Sense, le thread d'écriture doit d'abord exécuter A = 1, puis exécuter Flag = true. Lorsque le fil de lecture est en train de lire, i = 2;
Mais parce que A = 1 et Flag = True, il n'y a pas de corrélation logique. Par conséquent, il est possible d'inverser l'ordre d'exécution, et il est possible d'exécuter Flag = true d'abord, puis a = 1. À l'heure actuelle, lorsque Flag = True, passez au thread de lecture. À l'heure actuelle, A = 1 n'a pas encore été exécuté, alors le thread de lecture sera i = 1.
Bien sûr, ce n'est pas absolu. Il est possible qu'il y ait hors service et ne se produise pas.
Alors pourquoi est-il hors service? Cela commence par l'instruction du CPU. Une fois le code de Java compilé, il est finalement converti en code d'assemblage.
L'exécution d'une instruction peut être divisée en plusieurs étapes. En supposant que l'instruction du CPU est divisée en étapes suivantes
Supposons qu'il y ait deux instructions ici
De manière générale, nous penserons que les instructions sont exécutées en série, exécutez d'abord l'instruction 1, puis exécutent l'instruction 2. En supposant que chaque étape nécessite 1 période de processeur, puis l'exécution de ces deux instructions nécessite 10 périodes de processeur, ce qui est trop inefficace pour le faire. En fait, les instructions sont exécutées en parallèle. Bien sûr, lorsque la première instruction s'exécute si, la deuxième instruction ne peut pas fonctionner si, car l'instruction enregistre et similaire ne peut pas être occupée en même temps. Donc, comme indiqué dans la figure ci-dessus, les deux instructions sont exécutées en parallèle de manière relativement décalée. Lorsque l'instruction 1 exécute l'ID, l'instruction 2 exécute le if. De cette façon, deux instructions ont été exécutées en seulement 6 périodes de processeur, ce qui était relativement efficace.
Selon cette idée, jetons un coup d'œil à la façon dont l'instruction A = B + C est exécutée.
Comme le montre la figure, il y a une opération inactive (x) pendant l'opération d'ajout, car lorsque vous souhaitez ajouter B et C, lorsque l'opération X d'Add sur la figure, C n'a pas lu à partir de la mémoire (C ne lecture que de la mémoire lorsque l'opération MEM est terminée. Matériel, il n'est donc pas nécessaire d'attendre que le WB s'exécute avant que ADD soit effectué). Par conséquent, il y aura un temps inactif (x) en fonctionnement de l'ajout. Dans l'opération SW, comme l'instruction EX ne peut pas être effectuée simultanément avec l'instruction Add ex, il y aura un temps inactif (x).
Ensuite, donnons un exemple un peu plus compliqué
a = b + c
d = ef
Les instructions correspondantes sont les suivantes
La raison est similaire à ce qui précède, donc je ne l'analyserai pas ici. Nous avons constaté qu'il y avait beaucoup de X ici, et il y a beaucoup de cycles de temps perdu, et les performances sont également affectées. Existe-t-il un moyen de réduire le nombre de X?
Nous espérons utiliser certaines opérations pour remplir le temps libre de X, car ADD a une dépendance aux données avec les instructions ci-dessus, et nous espérons utiliser certaines instructions sans dépendance aux données pour remplir le temps libre généré par la dépendance aux données.
Nous avons changé l'ordre des instructions
Après avoir changé l'ordre des instructions, X est éliminé. La période d'exécution globale a également diminué.
La réorganisation des instructions peut rendre le pipeline plus fluide
Bien sûr, le principe du réarrangement des instructions est qu'il ne peut pas détruire la sémantique du programme série. Par exemple, a = 1, b = a + 1, ces instructions ne seront pas réorganisées car le résultat en série du réarrangement est différent de celui d'origine.
Le réarrangement des instructions n'est qu'un moyen d'optimiser le compilateur ou le CPU, et cette optimisation a causé des problèmes avec le programme au début de ce chapitre.
Comment le résoudre? Utilisez le mot-clé volatil, cette série suivante sera introduite.
3. Visibilité
La visibilité fait référence à la question de savoir si d'autres threads peuvent connaître immédiatement la modification lorsqu'un thread modifie la valeur d'une variable partagée.
Des problèmes de visibilité peuvent survenir dans divers liens. Par exemple, la réorganisation de l'instruction qui vient d'être mentionnée entraînera également des problèmes de visibilité, et en outre, l'optimisation du compilateur ou l'optimisation de certains matériels entraînera également des problèmes de visibilité.
Par exemple, un thread optimise une valeur partagée dans la mémoire, tandis qu'un autre thread optimise la valeur partagée dans le cache. Lors de la modification de la valeur en mémoire, la valeur mise en cache ne connaît pas la modification.
Par exemple, certaines optimisations matérielles, lorsqu'un programme écrit plusieurs fois sur la même adresse, il pense qu'il n'est pas nécessaire et ne conserve que la dernière écriture, de sorte que les données écrites auparavant seront invisibles dans d'autres threads.
En bref, la plupart des problèmes de visibilité découlent de l'optimisation.
Ensuite, regardons un problème de visibilité résultant du niveau de machine virtuelle Java
Le problème vient d'un blog
package edu.hushi.jvm; / ** * * @Author -10 * * / public class VisibilityTest étend Thread {private boolean stop; public void run () {int i = 0; while (! stop) {i ++; } System.out.println ("Loop de finition, i =" + i); } public void stopSit () {stop = true; } public boolean getStop () {return stop; } public static void main (String [] args) lève une exception {visibilitéTest v = new VisibilityTest (); v.start (); Thread.Sleep (1000); v.stopit (); Thread.Sleep (2000); System.out.println ("finition Main"); System.out.println (v.getStop ()); }} Le code est très simple. Le thread V maintient I ++ dans la boucle while jusqu'à ce que le thread principal appelle la méthode d'arrêt, modifiant la valeur de la variable d'arrêt dans le thread V pour arrêter la boucle.
Des problèmes se produisent lorsque le code apparemment simple s'exécute. Ce programme peut empêcher les threads de faire des opérations d'auto-incitation en mode client, mais en mode serveur, ce sera d'abord une boucle infinie. (Plus d'optimisation JVM en mode serveur)
La plupart des systèmes 64 bits sont en mode serveur et s'exécutent en mode serveur:
finition principale
vrai
Seules ces deux phrases seront imprimées, mais la boucle de finition ne sera pas imprimée. Mais vous pouvez constater que la valeur d'arrêt est déjà vraie.
L'auteur de ce blog utilise des outils pour restaurer le programme pour assembler le code
Seule une partie du code d'assemblage est interceptée ici, et la partie rouge est la partie de boucle. On peut clairement voir que seul 0x0193bf9d est la vérification de l'arrêt, tandis que la partie rouge ne prend pas la valeur d'arrêt, donc une boucle infinie est effectuée.
Ceci est le résultat de l'optimisation JVM. Comment l'éviter? Comme la réorganisation de directive, utilisez le mot-clé volatil.
Si un volatil est ajouté, restaurez-le dans le code d'assemblage et vous constaterez que chaque boucle obtiendra la valeur d'arrêt.
Ensuite, jetons un coup d'œil à quelques exemples dans la «spécification de la langue java»
La figure ci-dessus montre que la réorganisation des instructions conduira à différents résultats.
La raison pour laquelle R5 = R2 est fait dans la figure ci-dessus est que R2 = R1.x, R5 = R1.x, et il est directement optimisé à R5 = R2 au moment de la compilation. En fin de compte, les résultats sont différents.
4.
5. Le concept de sécurité des fils
Il fait référence au fait que lorsqu'une certaine fonction ou bibliothèque de fonctions est appelée dans un environnement multithread, il peut traiter correctement les variables locales de chaque thread et permettre à la fonction du programme d'être remplie correctement.
Par exemple, l'exemple I ++ mentionné au début
Cela conduira à un fil insécurisé.
Pour plus de détails sur la sécurité des fils, veuillez vous référer à ce blog que j'ai écrit auparavant, ou suivez la série suivante, et vous parlerez également de contenu connexe.