Multithreading. Modèle de mémoire Java (partie 2)

Bonjour, Habr! Je présente à votre attention la traduction de la deuxième partie de l'article "Java Memory Model" de Jakob Jenkov. La première partie est ici .



Architecture matérielle de la mémoire



L'architecture matérielle de la mémoire moderne est quelque peu différente du modèle de mémoire interne Java. Il est important de comprendre l'architecture matérielle afin de comprendre comment le modèle Java fonctionne avec elle. Cette section décrit l'architecture matérielle générale de la mémoire et la section suivante décrit le fonctionnement de Java.



Voici un schéma simplifié de l'architecture matérielle d'un ordinateur moderne:



Un ordinateur moderne a souvent 2 processeurs ou plus. Certains de ces processeurs peuvent également avoir plusieurs cœurs. Sur ces ordinateurs, plusieurs threads peuvent s'exécuter simultanément. Chaque processeur (note du traducteur - ci-après, l'auteur désigne probablement un cœur de processeur ou un processeur monocœur par un processeur)capable d'exécuter un thread à tout moment. Cela signifie que si votre application Java est multithread, alors dans votre programme, un thread peut être exécuté simultanément par processeur.



Chaque processeur contient un ensemble de registres qui sont essentiellement dans sa mémoire. Il peut effectuer des opérations sur les données dans les registres beaucoup plus rapidement que sur les données qui se trouvent dans la mémoire principale (RAM) de l'ordinateur. En effet, le processeur peut accéder à ces registres beaucoup plus rapidement.



Chaque CPU peut également avoir une couche de cache. En fait, la plupart des processeurs modernes l'ont. Un processeur peut accéder à sa mémoire cache beaucoup plus rapidement que la mémoire principale, mais généralement pas aussi vite que ses registres internes. Ainsi, la vitesse d'accès à la mémoire cache se situe quelque part entre la vitesse d'accès aux registres internes et à la mémoire principale. Certains processeurs peuvent avoir des caches à plusieurs niveaux, mais cela n'est pas important à comprendre pour comprendre comment le modèle de mémoire Java interagit avec la mémoire matérielle. Il est important de savoir que les processeurs peuvent avoir un certain niveau de mémoire cache.



L'ordinateur contient également une zone de mémoire principale (RAM). Tous les processeurs peuvent accéder à la mémoire principale. La zone de mémoire principale est généralement beaucoup plus grande que le cache du processeur.



En règle générale, lorsqu'un processeur a besoin d'accéder à la mémoire principale, il en lit une partie dans sa mémoire cache. Il peut également lire certaines données du cache dans ses registres internes, puis y effectuer des opérations. Lorsque le processeur a besoin d'écrire un résultat dans la mémoire principale, il vide les données de son registre interne vers la mémoire cache et, à un moment donné, vers la mémoire principale.



Les données stockées dans le cache sont généralement renvoyées dans la mémoire principale lorsque le processeur a besoin de stocker autre chose dans le cache. Le cache peut effacer sa mémoire et y écrire de nouvelles données en même temps. Le processeur n'a pas à lire / écrire le cache complet à chaque fois qu'il est mis à jour. En règle générale, le cache est mis à jour dans de petits blocs de mémoire 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 vidées vers la mémoire principale.



Combinaison du modèle de mémoire Java et de l'architecture de la mémoire matérielle



Comme mentionné, le modèle de mémoire Java et l'architecture matérielle de la mémoire sont différents. L'architecture matérielle ne fait pas la distinction entre pile de threads et tas. Sur le matériel, la pile de threads et le tas se trouvent dans la mémoire principale. Des portions de piles et de tas de threads peuvent parfois être présentes dans les caches et les registres internes du processeur. Ceci est illustré dans le diagramme:



Lorsque des objets et des variables peuvent être stockés dans différentes zones de la mémoire de l'ordinateur, certains problèmes peuvent survenir. Il en existe deux principaux:

• Visibilité des modifications apportées par le thread sur les variables partagées.

• Conditions de course lors de la lecture, de la vérification et de l'écriture de variables partagées.

Ces deux problèmes seront expliqués dans les sections suivantes.



Visibilité des objets partagés



Si deux ou plusieurs threads partagent un objet sans déclaration volatile ou synchronisation appropriée, les modifications apportées à l'objet partagé par un thread peuvent ne pas être visibles par les autres threads.



Imaginez qu'un objet partagé est initialement stocké dans la mémoire principale. Un thread s'exécutant sur un processeur lit un objet partagé dans le cache de ce même processeur. Là, il apporte des modifications à l'objet. Tant que le cache du processeur n'a pas été vidé dans la mémoire principale, la version modifiée de l'objet partagé n'est pas visible pour les threads exécutés sur d'autres processeurs. Ainsi, chaque thread peut obtenir sa propre copie de l'objet partagé, chaque copie sera dans un cache CPU séparé.



Le diagramme suivant illustre une esquisse de cette situation. Un thread s'exécutant sur le processeur gauche copie l'objet partagé dans son cache et modifie la valeur de la variablecountpar 2. Cette modification est invisible pour les autres threads exécutés sur le bon processeur car la mise à jour de n'a countpas encore été renvoyée dans la mémoire principale.



Afin de résoudre ce problème, vous pouvez utiliser volatilelors de la déclaration d'une variable. Il peut garantir qu'une variable donnée est lue directement depuis la mémoire principale et est toujours réécrite dans la mémoire principale lorsqu'elle est mise à jour.



Condition de course



Si deux ou plusieurs threads partagent le même objet et plusieurs variables de mise à jour de thread dans cet objet partagé, une condition de concurrence critique peut se produire .



Imaginez que le thread A lit une variable d' countobjet partagé dans le cache de son processeur. Imaginez aussi que le thread B fait la même chose, mais dans le cache d'un processeur différent. Maintenant, le thread A ajoute 1 à la valeur de la variable countet le thread B fait de même. Maintenant, il a var1été augmenté deux fois - séparément, +1 dans le cache de chaque processeur.



Si ces incréments étaient effectués séquentiellement, la variable countserait doublée et réécrite dans la mémoire principale + 2.

Cependant, les deux incréments ont été effectués simultanément sans synchronisation appropriée. Quel que soit le thread (A ou B) qui écrit sa version mise countà jour dans la mémoire principale, la nouvelle valeur sera seulement 1 de plus que la valeur d'origine, malgré deux incréments.



Ce diagramme illustre l'occurrence du problème de condition de concurrence décrit ci-dessus:



Pour résoudre ce problème, vous pouvez utiliser un bloc Java synchronisé... Un bloc synchronisé garantit qu'un seul thread peut entrer une section critique donnée de code à un moment donné. Les blocs synchronisés garantissent également que toutes les variables accédées dans un bloc synchronisé sont lues à partir de la mémoire principale, et lorsqu'un thread quitte un bloc synchronisé, toutes les variables mises à jour seront renvoyées dans la mémoire principale, que la variable soit déclarée volatileou non. ...



All Articles