CodeGym/Cours Java/Module 3/Mémoire dans la JVM, partie 2

Mémoire dans la JVM, partie 2

Disponible

Architecture matérielle de la mémoire

L'architecture matérielle de la mémoire moderne diffère du modèle de mémoire interne de Java. Par conséquent, vous devez comprendre l'architecture matérielle afin de savoir 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 comment Java fonctionne avec elle.

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

Architecture matérielle de la mémoire

Dans le monde moderne, un ordinateur possède 2 processeurs ou plus et c'est déjà la norme. Certains de ces processeurs peuvent également avoir plusieurs cœurs. Sur de tels ordinateurs, il est possible d'exécuter plusieurs threads en même temps. Chaque cœur de processeur est capable d'exécuter un thread à tout moment. Cela signifie que toute application Java est a priori multi-thread, et dans votre programme, un thread par cœur de processeur peut être exécuté à la fois.

Le cœur du processeur contient un ensemble de registres qui résident dans sa mémoire (à l'intérieur du cœur). Il effectue des opérations sur les données de registre beaucoup plus rapidement que sur les données qui résident 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 sa propre couche de cache. La plupart des processeurs modernes l'ont. Le processeur peut accéder à son cache beaucoup plus rapidement que la mémoire principale, mais pas aussi rapidement que ses registres internes. La valeur de la vitesse d'accès au cache est approximativement comprise entre les vitesses d'accès de la mémoire principale et des registres internes.

De plus, les processeurs ont une place pour avoir un cache à plusieurs niveaux. Mais ce n'est pas si important à savoir 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 cache.

Tout ordinateur contient également de la RAM (zone de mémoire principale) de la même manière. Tous les cœurs peuvent accéder à la mémoire principale. La zone de mémoire principale est généralement beaucoup plus grande que la mémoire cache des cœurs de processeur.

Au moment où le 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 effectuer des opérations dessus. Lorsque le processeur doit réécrire le résultat dans la mémoire principale, il vide les données de son registre interne dans le cache et, à un moment donné, dans la mémoire principale.

Les données stockées dans le cache sont normalement renvoyées dans la mémoire principale lorsque le processeur doit stocker autre chose dans le cache. Le cache a la capacité d'effacer sa mémoire et d'écrire des données en même temps. Le processeur n'a pas besoin de lire ou d'écrire le cache complet à chaque fois lors d'une mise à jour. Habituellement, le cache est mis à jour dans de petits blocs de mémoire, ils sont appelés "ligne 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 renvoyées dans la mémoire principale.

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

Comme déjà 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 les piles de threads et les tas. Sur le matériel, la pile de threads et HEAP (tas) résident dans la mémoire principale.

Des parties de piles et de tas de threads peuvent parfois être présentes dans les caches et les registres internes du CPU. Ceci est montré dans le schéma :

pile de threads et tas

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. Voici les deux principaux :

  • Visibilité des modifications apportées par le thread aux variables partagées.
  • Condition de concurrence lors de la lecture, de la vérification et de l'écriture de variables partagées.

Ces deux problèmes seront expliqués ci-dessous.

Visibilité des objets partagés

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

Imaginez qu'un objet partagé soit initialement stocké dans la mémoire principale. Un thread s'exécutant sur un CPU lit l'objet partagé dans le cache du même CPU. 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 schéma suivant illustre un aperçu de cette situation. Un thread s'exécutant sur le processeur de gauche copie l'objet partagé dans son cache et modifie la valeur de count à 2. Cette modification est invisible pour les autres threads s'exécutant sur le CPU de droite car la mise à jour de count n'a pas encore été renvoyée à la mémoire principale.

Pour résoudre ce problème, vous pouvez utiliser le mot clé volatile lors de la déclaration d'une variable. Il peut garantir qu'une variable donnée est lue directement à partir de la mémoire principale et est toujours réécrite dans la mémoire principale lors de la mise à jour.

Condition de course

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

Imaginez que le thread A lit la variable count de l'objet partagé dans le cache de son processeur. Imaginez aussi que le thread B fasse la même chose, mais dans le cache d'un autre processeur. Maintenant, le thread A ajoute 1 à la valeur de count, et le thread B fait de même. Maintenant, la variable a été augmentée deux fois - séparément de +1 dans le cache de chaque processeur.

Si ces incréments étaient exécutés séquentiellement, la variable de comptage serait doublée et réécrite dans la mémoire principale (valeur d'origine + 2).

Cependant, deux incréments ont été effectués en même temps sans synchronisation appropriée. Quel que soit le thread (A ou B) qui écrit sa version mise à jour de count dans la mémoire principale, la nouvelle valeur ne sera que de 1 de plus que la valeur d'origine, malgré les 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 le bloc synchronisé Java. Un bloc synchronisé garantit qu'un seul thread peut entrer dans une section de code critique donnée à un moment donné.

Les blocs synchronisés garantissent également que toutes les variables accédées à l'intérieur du bloc synchronisé seront lues à partir de la mémoire principale, et lorsque le thread quitte le bloc synchronisé, toutes les variables mises à jour seront renvoyées dans la mémoire principale, que la variable soit déclarée volatile ou No.

Commentaires
  • Populaires
  • Nouveau
  • Anciennes
Tu dois être connecté(e) pour laisser un commentaire
Cette page ne comporte pas encore de commentaires