Le modèle de mémoire Java, appelé JMM, est une garantie unifiée pour une série de plates-formes de machines virtuelles Java à la plate-forme spécifique non apparentée pour la visibilité de la mémoire et s'il peut être réorganisé dans un environnement multi-thread fourni par les développeurs. (Il peut y avoir une ambiguë en termes de terme et de distribution de mémoire de l'exécution de Java, qui fait référence à des zones de mémoire telles que le tas, la zone de méthode, la pile de threads, etc.).
Il existe de nombreux styles de programmation simultanée. En plus du CSP (processus séquentiel de communication), acteur et autres modèles, le modèle de mémoire partagé devrait être le modèle de mémoire partagé basé sur des threads et des verrous. Dans la programmation multi-thread, trois types de problèmes de concurrence doivent être prêts à l'attention:
・ Atomicité ・ Visibilité ・ Réorganiser
L'atomicité implique si d'autres threads peuvent voir l'état intermédiaire ou interférer lorsqu'un fil effectue une opération composite. En règle générale, c'est le problème de I ++. Deux threads effectuent des opérations ++ sur la mémoire de tas partagée en même temps. L'implémentation des opérations ++ dans JVM, Runtime et CPU peut être une opération composite. Par exemple, du point de vue des instructions JVM, il s'agit de lire la valeur de I de la mémoire du tas à la pile d'opérande, d'ajouter une et de réécrire à la mémoire du tas i. Au cours de ces opérations, s'il n'y a pas de synchronisation correcte, d'autres threads peuvent également l'exécuter en même temps, ce qui peut entraîner une perte de données et d'autres problèmes. Les problèmes d'atomicité courants, également connus sous le nom de l'état concurrentiel, sont jugés sur la base d'un éventuel résultat d'échec, tel que la lecture-modification-écriture. La visibilité et la réorganisation des problèmes découlent à la fois de l'optimisation du système.
Étant donné que la vitesse d'exécution du processeur et la vitesse d'accès de la mémoire sont sérieusement décalées, afin d'optimiser les performances, en fonction des principes de localisation tels que la localité temporelle et la localité spatiale, le CPU a ajouté un cache multi-couches entre la mémoire. Lorsqu'il est nécessaire de récupérer des données, le CPU ira d'abord au cache pour savoir s'il existe le cache correspondant. S'il existe, il sera retourné directement. S'il n'existe pas, il sera récupéré dans la mémoire et enregistré dans le cache. Désormais, les processeurs les plus cœurs sont devenus standard, chaque processeur a son propre cache, ce qui implique la question de la cohérence du cache. Les CPU ont des modèles de cohérence de différentes forces et faiblesses. La cohérence la plus forte est la sécurité la plus élevée, et elle est également conforme à notre mode de pensée séquentielle. Cependant, en termes de performances, il y aura beaucoup de frais généraux en raison de la nécessité d'une communication coordonnée entre différents CPU.
Un diagramme de structure de cache CPU typique est le suivant
Le cycle d'instructions du processeur est généralement la récupération des instructions, l'analyse des instructions pour lire les données, l'exécution d'instructions et la rédaction de données aux registres ou à la mémoire. Lors de l'exécution d'instructions en série, les données de lecture et stockées prennent longtemps, de sorte que le CPU utilise généralement le pipeline d'instructions pour exécuter plusieurs instructions en même temps pour améliorer le débit global, tout comme un pipeline d'usine.
La vitesse de lecture des données et de rédaction de données en mémoire n'est pas du même ordre de grandeur que d'exécuter des instructions, de sorte que le CPU utilise des registres et des caches comme caches et tampons. Lors de la lecture des données de la mémoire, il lira une ligne de cache (similaire à la lecture du disque et lira un bloc). Le module qui rédige les données mettra la demande de stockage dans un tampon de magasin lorsque les anciennes données ne sont pas dans le cache et continue d'exécuter l'étape suivante du cycle d'instructions. S'il existe dans le cache, le cache sera mis à jour et les données dans le cache se déroulent en mémoire en fonction d'une certaine politique.
classe publique MemoryModel {private int count; Arrêt booléen privé; public void initCountAndStop () {count = 1; stop = false; } public void doloop () {while (! stop) {count ++; }} public void printResult () {System.out.println (count); System.out.println (stop); }}Lors de l'exécution du code ci-dessus, nous pouvons penser que Count = 1 sera exécuté avant stop = false. Ceci est correct dans l'état idéal illustré dans le diagramme d'exécution du CPU ci-dessus, mais il est incorrect lorsque l'on considère le registre et la tampon du cache. Par exemple, l'arrêt lui-même est dans le cache mais le nombre n'est pas là, alors l'arrêt peut être mis à jour et le tampon d'écriture de nombre est actualisé en mémoire avant de réécrire.
De plus, le CPU et le compilateur (se référer généralement à JIT pour Java) peuvent modifier l'ordre d'exécution des instructions. Par exemple, dans le code ci-dessus, Count = 1 et Stop = False n'ont pas de dépendances, de sorte que le CPU et le compilateur peuvent modifier l'ordre de ces deux. De l'avis d'un programme unique, le résultat est le même. C'est également le SI-Sérial que le CPU et le compilateur doivent assurer (quelle que soit la façon dont l'ordre d'exécution est modifié, le résultat d'exécution du seul thread reste inchangé). Étant donné que la majeure partie de l'exécution du programme est unique, une telle optimisation est acceptable et apporte de grandes améliorations de performances. Cependant, dans le cas du multithreading, des résultats inattendus peuvent se produire sans les opérations de synchronisation nécessaires. Par exemple, après que Thread T1 exécute la méthode initCountAndStop, Thread T2 exécute Printresult, qui peut être 0, false, 1, false ou 0, vrai. Si Thread T1 exécute d'abord Doloop () et que le thread T2 exécute InitCountAndStop une seconde, alors T1 peut sauter de la boucle, ou il peut ne jamais voir la modification de l'arrêt en raison de l'optimisation du compilateur.
En raison des divers problèmes dans les situations multiples ci-dessus, la séquence de programme dans le multi-threading n'est plus l'ordre d'exécution et entraîne le mécanisme sous-jacent. Le langage de programmation doit offrir aux développeurs une garantie. En termes simples, cette garantie est lorsque la modification d'un thread sera visible par d'autres threads. Par conséquent, la langue Java propose JavamemoryModel, c'est-à-dire le modèle de mémoire Java, qui nécessite une implémentation conformément aux conventions de ce modèle. Java fournit des mécanismes tels que volatile, synchronisée et finale pour aider les développeurs à assurer l'exactitude des programmes multi-threads sur toutes les plateformes de processeur.
Avant JDK1.5, le modèle de mémoire de Java a eu de graves problèmes. Par exemple, dans l'ancien modèle de mémoire, un thread peut voir la valeur par défaut d'un champ final une fois le constructeur terminé, et l'écriture du champ volatil peut être réorganisée avec la lecture et l'écriture du champ non volatile.
Ainsi, dans JDK1.5, un nouveau modèle de mémoire a été proposé via JSR133 pour résoudre les problèmes précédents.
Réorganiser les règles
Verrouillage volatil et moniteur
| Est-il possible de réorganiser | La deuxième opération | La deuxième opération | La deuxième opération |
|---|---|---|---|
| La première opération | Lecture normale / écriture ordinaire | lecture / moniteur volatile entre | sortie d'écriture / moniteur volatile |
| Lecture normale / écriture ordinaire | Non | ||
| VOALTILE LEA / MONITEUR ENTRÉE | Non | Non | Non |
| sortie d'écriture / moniteur volatile | Non | Non |
La lecture normale fait référence à la télécharge de Getfield, GetStatic et non volatile, et la lecture normale se réfère à l'arrière des tableaux putfield, putstatiques et non volatiles.
La lecture et l'écriture de champs volatils sont Getfield, GetStatic, Putfield, PutStatic, respectivement.
Le surveillant consiste à entrer le bloc de synchronisation ou la méthode de synchronisation, le monitorexiste se réfère à la sortie du bloc de synchronisation ou de la méthode de synchronisation.
Non dans le tableau ci-dessus, fait référence à deux opérations qui ne permettent pas de réorganiser. Par exemple (écriture normale, écriture volatile) fait référence à la réorganisation des champs non volatils et à la réorganisation des écritures de tout champ volatil ultérieur. Lorsqu'il n'y a pas de non, cela signifie que la réorganisation est autorisée, mais le JVM doit garantir une sécurité minimale - la valeur de lecture est soit la valeur par défaut, soit écrite par d'autres threads (les opérations de lecture et d'écriture longues 64 bits sont un cas spécial. Lorsqu'il n'y a pas de modification volatile, il n'est pas garanti que la lecture et l'écriture sont atomiques, et la couche sous-jacente peut le diviser en deux opérations distinctes).
Champ final
Il existe deux règles spéciales supplémentaires pour le champ final
Ni l'écriture du champ final (dans le constructeur) ni l'écriture de la référence de l'objet de champ final lui-même ne peuvent être réorganisées avec des écritures ultérieures des objets tenant le champ final (en dehors du constructeur). Par exemple, l'énoncé suivant ne peut pas être réorganisé
X.FinalField = V; ...; sharedRef = x;
La première charge du champ final ne peut pas être réorganisée avec l'écriture de l'objet tenant le champ final. Par exemple, l'énoncé suivant ne permet pas de réorganiser.
x = sharedRef; ...; i = x.Finalfield
Barrière de mémoire
Les processeurs prennent tous en charge certaines barrières ou clôtures de mémoire pour contrôler la visibilité de la réorganisation et des données entre les différents processeurs. Par exemple, lorsque le CPU réécrit les données, il mettra la demande du magasin dans le tampon d'écriture et attendra la chasse en mémoire. Cette demande de magasin peut être empêchée d'être réorganisée avec d'autres demandes en insérant la barrière pour assurer la visibilité des données. Vous pouvez utiliser un exemple de vie pour comparer la barrière. Par exemple, lorsque vous prenez un ascenseur de pente dans le métro, tout le monde entre dans l'ascenseur en séquence, mais certaines personnes vont faire le tour de la gauche, de sorte que l'ordre lors de la sortie de l'ascenseur est différent. Si une personne transporte un gros bagage bloqué (barrière), les gens derrière ne peuvent pas contourner :). De plus, la barrière ici et la barrière d'écriture utilisée dans GC sont des concepts différents.
Classification des barrières de mémoire
Presque tous les processeurs prennent en charge les instructions de la barrière d'un certain grain grossier, généralement appelés clôtures (clôture, clôture), ce qui peut garantir que les instructions de charge et de stockage initiées avant la clôture peuvent être strictement en ordre avec la charge et le stockage après la clôture. Habituellement, il sera divisé en quatre types de barrières suivants en fonction de leur objectif.
Barrières de chargement
Charge1; Charge de chargement; Load2;
Assurez-vous que les données de charge1 sont chargées avant le chargement 2 et après le chargement
Barrières Storestore
Magasin1; Storestore; Magasin2
Assurez-vous que les données de Store1 sont visibles pour d'autres processeurs avant le magasin2 et après.
Barrières de chargement
Charge1; LoadStore; Magasin2
Assurez-vous que les données de Load1 sont chargées avant le magasin2 et après la rinçage des données
Barrières à la localisation
Magasin1; Storeload; Charge2
Assurez-vous que les données de Store1 sont visibles devant d'autres processeurs (tels que Flushing to Memory) avant de charger les données dans Load2 et après la charge. La barrière Storeload empêche le chargement de lire les anciennes données plutôt que des données récemment écrites par d'autres processeurs.
Presque tous les multiprocesseurs à l'époque moderne nécessitent du storeload. Les frais généraux de Storeload sont généralement les plus importants, et Storeload a l'effet de trois autres barrières, de sorte que Storeload peut être utilisé comme barrière générale (mais des frais généraux).
Par conséquent, en utilisant la barrière de mémoire ci-dessus, les règles de réorganisation dans le tableau ci-dessus peuvent être implémentées
| Besoin d'obstacles | La deuxième opération | La deuxième opération | La deuxième opération | La deuxième opération |
|---|---|---|---|---|
| La première opération | Lecture normale | Écriture normale | lecture / moniteur volatile entre | sortie d'écriture / moniteur volatile |
| Lecture normale | Standard | |||
| Lecture normale | Storestore | |||
| VOALTILE LEA / MONITEUR ENTRÉE | Charge de chargement | Standard | Charge de chargement | Standard |
| sortie d'écriture / moniteur volatile | Insérer | Storestore |
Afin de prendre en charge les règles des champs finaux, il est nécessaire d'ajouter une barrière à l'écriture finale à la finale
X.FinalField = V; Storestore; sharedRef = x;
Insérer une barrière de mémoire
Sur la base des règles ci-dessus, vous pouvez ajouter une barrière au traitement des champs volatils et des mots clés synchronisés pour respecter les règles du modèle de mémoire.
Insérez le storestore avant la barrière volatile des magasins après que tous les champs finaux soient écrits mais insérez le stagrestore avant le retour du constructeur
Insérez la barrière de la lacet après un magasin volatil. Insérez la barrière de chargement et de chargement de charge après une charge volatile.
Le moniteur entre et les règles de charge volatile sont cohérents et les règles de sortie du moniteur et de magasin volatil sont cohérentes.
Arriver avant
Les diverses barrières de mémoire mentionnées ci-dessus sont encore relativement complexes pour les développeurs, donc JMM peut utiliser une série de règles de relations d'ordre partiel de Contant avant d'illustrer. Pour s'assurer que le thread qui exécute l'opération B voit le résultat de l'opération A (indépendamment du fait que A et B soient exécutés dans le même thread), alors la relation Contant avant doit être respectée entre A et B, sinon le JVM peut les réorganiser arbitrairement.
La liste des règles
Les règles HappendFore incluent
Règles de séquence de programme: Si l'opération A dans le programme est avant l'opération B, l'opération A dans le même thread effectuera les règles de verrouillage du moniteur avant l'opération B: L'opération de verrouillage sur le verrouillage du moniteur doit être effectuée avant l'opération de verrouillage sur le même verrouillage du moniteur.
Règles de variable volatile: l'opération d'écriture de la variable volatile doit exécuter des règles de démarrage de thread avant l'opération de lecture de la variable: l'appel vers le thread.Les démarrage sur le thread doivent exécuter les règles de fin du thread avant toute opération dans le thread: toute opération dans le thread doit exécuter les règles d'interruption avant que les autres threads ne détectent que l'interruption de thread se termine: lorsqu'une interruption des appels de threads sur une autre opération avant l'exécution de la passivité et de l'interrupteur ne détient pas l'interruption: If Exécution Bou L'opération B est exécutée avant l'opération C, puis l'opération A est exécutée avant l'opération C.
Le verrouillage d'affichage a la même sémantique de mémoire que le verrouillage du moniteur, et la variable atomique a la même sémantique de mémoire que la volatile. L'acquisition et la libération de verrous, les opérations de lecture et d'écriture de variables volatiles satisfont la relation d'ordre complet, de sorte que l'écriture de volatils peut être effectuée avant les lectures volatiles ultérieures.
L'évaluation susmentionnée peut être combinée à l'aide de plusieurs règles.
Par exemple, après le thread A, il entre dans le verrouillage du moniteur, l'opération avant de libérer le verrouillage du moniteur est basée sur les règles de séquence du programme, et l'opération de libération du moniteur est utilisée pour obtenir le même verrouillage de moniteur dans le thread B ultérieur, et l'opération dans l'opération dans CELLAFEFOR et le thread B.
Résumer
Ce qui précède est toute l'explication détaillée du modèle de mémoire Java JMM dans cet article, j'espère que ce sera utile à tout le monde. S'il y a des lacunes, veuillez laisser un message pour le signaler. Merci vos amis pour votre soutien pour ce site!