Maison >Java >javaDidacticiel >Introduction détaillée au modèle de mémoire Java
Ce modèle de mémoire Java spécifie comment la machine virtuelle Java fonctionne avec la mémoire de l'ordinateur (RAM). Cette machine virtuelle Java est un modèle de l'ordinateur entier, de sorte que ce modèle inclut naturellement un modèle de mémoire - également appelé modèle de mémoire Java.
Comprendre le modèle de mémoire Java est important si vous souhaitez concevoir correctement des programmes concurrents. Ce modèle de mémoire Java fait référence à comment et quand différents threads peuvent voir les valeurs des variables partagées écrites par d'autres threads et à la manière d'accéder aux variables partagées de manière synchrone.
Le modèle de mémoire Java d'origine était insuffisant, à tel point que le modèle de mémoire Java a été amélioré dans Java 1.5. Cette version du modèle de mémoire Java est toujours utilisée dans Java 8.
Modèle de mémoire Java interne
Le modèle de mémoire Java est utilisé à l'intérieur de la JVM en le divisant en piles de threads et en tas. Ce diagramme examine le modèle de mémoire d'un point de vue logique :
Chaque thread exécuté dans la machine virtuelle Java possède sa propre pile de threads. La pile de threads contient des informations sur les méthodes que ce thread a appelées jusqu'au point d'exécution actuel. Nous appellerons également cela la « pile d’appels ». Au fur et à mesure que le thread exécute son code, cette pile d'appels change.
Cette pile de threads contiendra également toutes les variables locales pour chaque méthode en cours d'exécution (toutes les méthodes de la pile d'appels). Un thread ne peut accéder qu’à sa propre pile de threads. Les variables locales créées par un thread ne sont pas visibles par tous les autres threads. Même si deux threads exécutent exactement le même code, les deux threads créent toujours leurs propres variables locales. Par conséquent, chaque thread possède sa propre version des variables locales.
Toutes les variables locales de types basiques (boolean, byte, short, char, int, long, float, double) sont entièrement stockées dans la pile de threads et sont donc invisibles pour les autres threads. Un thread peut transmettre une copie d'une variable de type primitif à un autre thread, mais il ne peut toujours pas partager une variable locale de type primitif.
Ce tas contient tous les objets créés dans votre application, quel que soit le thread qui a créé l'objet. Cela inclut les versions d'objet des types de base (par exemple, Byte, Integer, Long, etc.). Qu'un objet soit créé et affecté à une variable locale ou qu'une variable membre d'un autre objet soit créée, l'objet est toujours stocké dans le tas.
Voici un diagramme montrant cette pile d'appels et les variables locales stockées dans la pile de threads, ainsi que les objets stockés dans le tas :
a Le La variable locale peut être un type primitif, auquel cas elle sera entièrement stockée dans la pile de threads.
Une variable locale peut être une référence d'objet. Dans ce scénario, la référence (variable locale) est stockée dans la pile de threads, mais l'objet lui-même est stocké dans le tas.
Un objet peut contenir des méthodes, et ces méthodes contiennent des variables locales. Ces variables locales sont également stockées dans la pile de threads, même si l'objet auquel appartient cette méthode est stocké dans le tas.
Les variables membres d'un objet sont stockées dans le tas avec l'objet lui-même. Non seulement lorsque cette variable membre est de type basique, mais aussi s'il s'agit d'une référence à un objet.
Les variables de classe statiques sont également stockées dans le tas.
Un objet dans le tas est accessible à tous les threads qui ont une référence à cet objet. Lorsqu'un thread accède à un objet, il peut également accéder aux variables membres de l'objet. Si deux threads appellent une méthode sur le même objet en même temps, ils accéderont aux variables membres de l'objet en même temps, mais chaque thread aura sa propre copie des variables locales.
Voici une illustration basée sur la description ci-dessus :
Deux threads ont un ensemble de variables locales. L'une des variables locales (Locale Variable 2) pointe vers un objet commun dans le tas (Object 3). Les deux threads ont chacun une référence différente au même objet. Les variables locales auxquelles elles font référence sont stockées dans la pile de threads, mais le même objet pointé par ces deux références différentes se trouve dans le tas.
Notez comment cet objet partagé (Objet 3) fait référence à l'Objet 2 et à l'Objet 4 en tant que variables membres (indiquées par les flèches dans l'illustration). Grâce aux références de ces variables dans Object3, les deux threads peuvent également accéder à Object2 et Object4.
Ce diagramme montre également une variable locale pointant vers deux objets différents dans le tas. Dans ce scénario, cette référence pointera vers deux objets différents (objet 1 et objet 5), et non vers le même objet. En théorie, deux objets peuvent accéder à la fois à l'objet 1 et à l'objet 5, si les deux threads ont des références à ces deux objets. Mais dans le diagramme, chaque thread n'a qu'une référence à ces deux objets.
Alors, quel type de code aura la structure de mémoire dans l'image ci-dessus ? Eh bien, une réponse courte comme le code suivant :
public class MyRunnable implements Runnable() { public void run() { methodOne(); } public void methodOne() { int localVariable1 = 45; MySharedObject localVariable2 = MySharedObject.sharedInstance; //... do more with local variables. methodTwo(); } public void methodTwo() { Integer localVariable1 = new Integer(99); //... do more with local variable. } }
public class MySharedObject { //static variable pointing to instance of MySharedObject public static final MySharedObject sharedInstance = new MySharedObject(); //member variables pointing to two objects on the heap public Integer object2 = new Integer(22); public Integer object4 = new Integer(44); public long member1 = 12345; public long member1 = 67890; }
Si deux threads exécutent cette méthode d'exécution, cette icône affichera le résultat plus tôt. La méthode run appelle la méthode methodOne et la méthode methodOne appelle la méthode methodTwo.
La méthode methodOne déclare une variable locale de type basique (type int), et une variable locale de référence d'objet.
Lorsque chaque thread exécute la méthode methodOne, il crée ses propres copies de localVariable1 et localVariable2 dans leurs piles de threads respectives. Ce localVariable1 sera complètement séparé les uns des autres et survivra simplement dans leurs piles de threads respectives. Un thread ne peut pas voir les modifications apportées à localVariable1 par un autre thread.
Chaque thread exécutant la méthode methodOne créera également sa propre copie de localVariable2. Cependant, ces deux copies différentes de localVariable2 pointent vers le même objet dans le tas. Ce code définit localVariable2 pour pointer vers une référence à un objet via une variable statique. Il n’existe qu’une seule copie de la variable statique, et cette copie se trouve dans le tas. Par conséquent, les deux copies dans localVariable2 finissent par pointer vers la même instance. Ce MySharedObject est également stocké dans le tas. C'est l'équivalent de l'objet 3 dans l'image ci-dessus.
Notez que cette classe MySharedObject contient également deux variables membres. Les variables membres elles-mêmes sont stockées dans le tas avec l'objet. Ces deux variables membres pointent vers deux autres objets Integer. Ces objets Integer sont équivalents à l'objet 2 et à l'objet 4 dans la figure ci-dessus.
Notez également comment la méthode methodTwo crée une variable locale de localVariable1. Cette variable locale est une référence à un objet Integer. Cette méthode définit la référence localVariable1 pour qu'elle pointe vers une nouvelle instance d'Integer. Cette référence localVariable1 sera stockée dans une copie de chaque thread dans la méthode d'exécution de methodTwo. Les deux objets Integer instanciés seront stockés dans le tas, mais un nouvel objet Integer sera créé à chaque exécution de cette méthode, et les deux threads exécutant cette méthode créeront des instances Integer distinctes. Les objets Integer créés dans la méthode methodTwo sont équivalents à l'objet 1 et à l'objet 5 dans la figure ci-dessus.
Notez également que les deux variables membres de type long dans la classe MySharedObject sont des types de base. Étant donné que ces variables sont des variables membres, elles sont toujours stockées dans le tas avec l'objet. Seules les variables locales seront stockées dans la pile de threads.
Architecture de la mémoire matérielle
L'architecture de la mémoire matérielle actuelle est légèrement différente du modèle de mémoire interne Java. Il est également important de comprendre l’architecture matérielle de la mémoire et il est utile de comprendre le fonctionnement du modèle de mémoire Java. Cette section décrit la structure de mémoire matérielle commune et les sections suivantes décrivent comment le modèle de mémoire Java fonctionne avec cette structure.
Voici un schéma simplifié de la structure matérielle d'un ordinateur moderne :
Les ordinateurs modernes ont souvent deux processeurs ou plus. Certains de ces processeurs peuvent avoir plusieurs cœurs. Le point important est que les ordinateurs dotés de deux processeurs ou plus peuvent avoir plusieurs threads exécutés en même temps. Chaque processeur peut exécuter un thread à tout moment. Dans votre application Java, un thread peut s'exécuter sur chaque processeur en même temps.
Chaque CPU contient une série de registres, qui sont essentiellement de la mémoire CPU. Ce processeur s'exécute plus rapidement sur les registres que sur la mémoire principale. En effet, le processeur accède aux registres plus rapidement que la mémoire principale.
Chaque processeur peut également avoir une couche mémoire pour le cache du processeur. En fait, la plupart des processeurs modernes disposent d’une couche de mémoire cache d’une certaine taille. Ce processeur accède à la couche de mémoire cache beaucoup plus rapidement que la mémoire principale, mais pas aussi vite que l'accès aux registres internes. En conséquence, la vitesse d'accès à cette mémoire cache du CPU se situe entre les registres internes et la mémoire principale. Certains processeurs peuvent avoir plusieurs niveaux de cache (niveau 1 et niveau 2), mais il n'est pas important de le savoir pour comprendre l'interaction du modèle de mémoire Java avec la mémoire. Il est important de savoir que le CPU peut avoir une couche de mémoire cache.
Un ordinateur contient également une zone de mémoire principale (RAM). Tous les processeurs ont accès à cette mémoire principale. Cette mémoire principale est généralement plus grande que la mémoire cache du processeur.
En tant que représentant, lorsque le CPU doit accéder à la mémoire principale, il lira la partie mémoire principale dans le cache du CPU. Il peut même lire des parties du cache dans des registres, puis y effectuer des opérations. Lorsque le processeur doit réécrire le résultat dans la mémoire principale, il videra la valeur du registre interne vers la mémoire cache et, à un moment donné, videra la valeur dans la mémoire principale.
Ces valeurs stockées dans la mémoire cache sont vidées vers la mémoire principale lorsque le processeur doit y stocker autre chose. Ce cache CPU peut parfois être écrit dans une partie de sa mémoire, et parfois une partie de sa mémoire peut être vidée. Elle n'a pas besoin de lire et d'écrire l'intégralité du cache à chaque fois. Généralement, ce cache est mis à jour dans des blocs de mémoire plus petits appelés « lignes de cache ». Une ou plusieurs lignes de cache peuvent être lues dans la mémoire cache, et une ou plusieurs lignes de cache peuvent être à nouveau vidées dans la mémoire principale.
Combler le fossé entre le modèle de mémoire Java et la structure de mémoire matérielle
Comme déjà mentionné, le modèle de mémoire Java et la structure de mémoire matérielle sont différents. Cette structure de mémoire matérielle ne fait pas de distinction entre les piles de threads et les tas. Dans le matériel, la pile de threads et le tas sont situés dans la mémoire principale. Des parties de la pile de threads et du tas peuvent parfois apparaître dans le cache du processeur et dans les registres internes du processeur, comme le montre la figure suivante :
Lorsque les objets et variables Certains problèmes peut se produire lorsque les données peuvent être stockées dans différentes zones de mémoire de votre ordinateur. Les deux principaux problèmes sont :
Visibilité du fil de discussion pour les mises à jour des variables partagées
Lors de la lecture des conditions de course pour récupérer, vérifier et écrire des variables partagées
Ces problèmes seront expliqués dans les sections suivantes.
Visibilité des objets partagés
Si deux Ou si plus de threads partagent un objet, sans utilisation appropriée des déclarations volatiles ou de la synchronisation, les variables partagées mises à jour par un thread peuvent ne pas être visibles par les autres threads.
Imaginez que l'objet partagé soit initialement stocké dans la mémoire principale. Un thread exécuté sur le CPU lit l'objet partagé dans son cache CPU. Ici, il modifie un objet partagé. Tant que le cache du processeur n'est pas vidé dans la mémoire principale, la version modifiée de cet objet partagé n'est pas visible pour les threads exécutés sur d'autres processeurs. De cette façon, chaque thread peut se retrouver avec sa propre copie de l'objet partagé, chaque copie étant située dans un cache CPU différent.
Le schéma ci-dessous illustre la situation schématique. Un thread exécuté sur le processeur gauche copie la variable partagée dans le cache du processeur et modifie sa valeur à 2. Cette modification n'est pas visible pour les autres threads exécutés sur le bon processeur, car la mise à jour à compter n'a pas encore été renvoyée dans la mémoire principale.
Pour résoudre ce problème, vous pouvez utiliser le mot-clé volatile de Java. Ce mot-clé garantit qu'une variable donnée est lue directement depuis la mémoire principale et écrite directement dans la mémoire principale lors de la mise à jour.
Condition de concurrence
Si deux threads ou plus partagent un objet et que plusieurs threads mettent à jour une variable dans l'objet partagé, les conditions de concurrence peut survenir.
Imaginez si le thread A lit la variable count d'un objet partagé dans son cache CPU. Pendant ce temps, le thread B fait la même chose, mais va dans un cache CPU différent. Désormais, les incréments de thread comptent par un et le thread B fait la même chose. Maintenant, la variable est incrémentée deux fois.
Si ces incréments sont effectués séquentiellement, la variable de comptage sera incrémentée deux fois et plus 2 sera écrit dans la mémoire principale en fonction de la valeur d'origine.
Ensuite, les deux incréments ne sont pas correctement synchronisés, ce qui entraîne une exécution simultanée. Que le thread A ou le thread B écrive sa mise à jour dans la mémoire principale, la valeur de cette mise à jour n'est augmentée que de 1, et non de 2.
Ce diagramme montre le problème avec la condition de concurrence décrite ci-dessus :
Pour résoudre ce problème, vous pouvez utiliser les verrous de synchronisation Java. Un verrou de synchronisation peut garantir qu'un seul thread peut accéder à la zone critique du code à tout moment. Le verrou de synchronisation garantira également que tous les accès aux variables seront lus à partir de la mémoire principale, et lorsque le thread quittera le bloc de code synchronisé, toutes les variables mises à jour seront à nouveau renvoyées dans la mémoire principale, que la variable soit déclarée volatile ou non.
Ce qui précède est l'introduction détaillée du modèle de mémoire Java. Pour plus de contenu connexe, veuillez faire attention au site Web PHP chinois (www.php.cn) !