Posts Tagged ‘java’

Modele Mémoire Java vs Physique

Tuesday, January 12th, 2016

La JVM est un modèle complet d’ordinateur, il inclus notamment celui de la mémoire. Mais en pratique cela peut-être très différent du modèle physique et avoir un impact sur le programme. Un modèle mémoire, quant à lui, décrit les conditions dans lesquels une écriture faite par un thread sera visible par un autre thread faisant une lecture. La plupart des développeurs pensent que l’écriture d’une variable quelconque sans aucune synchronisation sera visible de n’importe quel thread faisant une lecture (on appelle se comportement “sequential consistency” ) . Et pourtant, il n’y a pas que la mémoire, le cache etc., qui peuvent poser des problèmes de mémoires, il y a aussi, par exemple, le compilateur. Il est possible par exemple que l’optimisation faite par un compilateur casse une application ! C’est pourquoi il est utile de mieux comprendre le modèle de mémoire proposé par la JVM.
Le modèle mémoire de la JVM.
Le modèle interne de la JVM est relativement simple. Il se décompose en deux grande partie : la pile (heap) et la pile de thread. Chaque thread qui tourne possède sa propre pile, qui contient les appels#160; mais aussi les variables locales de type primitif (ex: int) . Cela implique par exemple que chaque thread à sa propre version d’une variable locale.
La pile (heap) contient elle tous les objets crée par l’application et ce quelque soit le thread qui l’a crée (incluant aussi les objets comme Integer ).
On peut résumer ainsi :
* variable local primitive : stocker dans la pile local
* variable local “objet” : la réference est dans la pile local, l’objet est dans la Heap.
Il est à noté que de ce fait, tous les objets de la Heap peuvent être accéder par n’importe quel thread, à partir du moment ou on a une référence.
Prenons par exemple 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;
}

2 threads déclarent des variables locales, dont l’une pointe ver un objet partagé sur la Heap. les références sont stocké dans chaque pile, mais l’objet est le même sur la Heap. Résultat, les objets 2 et 4 sont accessible par les 2 threads, par contre, les objets 1 et 5 ne le sont pas.

Le modèle physique
Venons en maintenant au modèle physique. De nos jour, un ordinateur est généralement constitué de plusieurs CPU souvent eux même constitué de plusieurs cœurs. Chaque cœur peux faire tourner un thread de façon indépendante. Chaque cœur possède également ses propres registres et de la mémoire cache, de même que le CPU (L1/L2/L3…). Bien évidement, un ordinateur dispose aussi de mémoire RAM commune et accessible à tous. Point important : quand un cœur à besoin d’un élément en RAM, il va le lire dans l’un de ses caches puis dans ses registres. A l’inverse, pour mettre à jour, il va d’abord écrire dans le cache, puis plus tard l’écrire en mémoire. Souvent, ces deux mécanisme ne se font pas unitairement, mais par ce que l’on appel “cache line” ce qui implique la lecture ou l’écriture de plusieurs valeurs en même temps.
cpu-diagram
Rassemblons les deux modèles
Maintenant que nous avons une vision des deux modèles, on peut repérer les différences. il est clair par exemple que le hardware ne fait pas la différence Heap/Stack. On peut aussi comprendre qu’a un instant donné des variables ou de morceau de Heap peuvent se retrouver dans différents endroits : cache, registre, RAM etc …
java-memory-model-5
Et c’est la que les problèmes peuvent arriver !
On peut les classer en deux grandes catégories :
* les problèmes de visibilités
* les “race conditions”
Problème de Visibilité
Imaginons un objet qui se trouve dans la Heap. Initialement stocké dans la RAM. Imaginons qu’un thread souhaite y accéder, comme on l’a vu, l’objet va se retrouver dans le cache d’un CPU. Supposons que l’on fasse des modifications sur cet objet. Tant que le CPU n’aura pas vidé sont cache en RAM, les modifications ne seront pas visible par les autres CPUs qui accèderait à la valeur ! C’est la qu’intervient la notion de volatile qui force la lecture mais aussi l’écriture en RAM de la variable. On appelle aussi cela une “Memory Barrier”, en assembleur se sont des instructions qui force l’ordre d’exécution de certaines actions. Notons que dans certains cas il n’est même pas nécessaire d’avoir plusieurs thread pour être dans ce genre de cas les optimisation du compilateur peuvent avoir ce genre d’effet de bord.
Si on résume : le mot clef volatile indique à la JVM de produire les instructions permettant au processeur de garantir la cohérence de la mémoire RAM.
Attention quand même d’après la JLS :

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

ce qui veux dire que l’on parle d’ordre et pas de temps cpu, et qu’il y a donc des cas ou volatile seul ne suffit pas.
Pour plus de détail vous pouvez vous référer aux $17.4.4 “Synchronization order” et#160; §17.4.5 ‘’”happens-before order” de la JLS
Les Race Conditions
Imaginons cette fois ci que les deux threads font l’opération simultanément.#160; Cette fois, les valeurs sont bonnes, mais le résultat final ne l’est pas. ex: si on a fait 2 additions, on espère avoir 3#160; (1+1+1 !) or on aura 2 (1+1) !
La solution ? la synchronisation ! Un bloc dit synchronisé garantie qu’un seul thread peut entrée dans cette section “critique” à un instant donnée. Au passage, il garantie aussi que toutes les variables seront lues depuis la RAM, et qu’à la sortie du bloc, toutes les variables modifiées seront écrite en RAM et ce qu’elles soit volatile ou non.