JavaPractices est un site web qui propose des idées et des astuces pour Java. Chaque article présente un point avec quelques lignes de code,
une explication claire et concise. Cependant je ne suis pas d’accord avec tout les articles.
Ainsi celui qui parle de Lazy Initialization/Instantiation m’a rappelé un autre article dont je vais parler. Avant cela, voyons déjà qu’est-ce que le processus de Lazy Initialization
L’instantiation retardée (baaah) ou Lazy Initialization est une petite astuce qui permet d’économiser de la mémoire. Lorsqu’une proprieté d’un objet n’est pas systématiquement utilisée, vous pouvez l’initialiser que lorsque celle-ci sera appelée. Comment expliquer cela par le code ? Prenons une class Monster avec les attributs color et size. Nous décidons que la couleur du monstre a dépendre de sa taille. Notre accesseur, getColor, retournera une couleur rouge si le monstre fait plus de 50m, sinon le monstre sera bleu. Ok c’est pas très original mais je n’ai pas l’inspiration.
Le code que je vous propose sera le suivant:
import java.awt.*; /** * Monster is a dangerous animal caracterized with a color and a size. * The color depends on the size attribute * * @author Nicolas Martignole * @version Jun 1, 2005 1:17:49 PM */ public class Monster { private int size; private Color color; /** * Creates a Monster. * @param size is the preferred monster size */ public Monster(final int size){ this.size=size; } public int getSize(){ return size; } /** * Returns the color attribute. * @return a safe copy of the color attribute. */ public Color getColor(){ if(color==null){ if(size<50) { color=Color.RED; }else{ color=Color.BLUE; } } return color; } }
Le « Lazy Initialization » est la technique qui permet ici de dire « calcule et stocke la couleur uniquement si la méthode getColor est appelée ». Sinon, l’attribut color ne sera pas initialisé.
Premier problème: 2 Threads appelent getColor en même temps et donc la première peut être entrain d’initialiser l’attribut couleur et la deuxième va retourner null, ou refaire l’initialisation… Bref la solution semble être la synchronisation. Le code deviendra alors le suivant:
/** * Step 2 * Returns the color attribute. * @return a safe copy of the color attribute. */ public synchronized Color getColor(){ if(color==null){ if(size<50) { color=Color.RED; }else{ color=Color.BLUE; } } return color; }
Quel est alors le problème ? Imaginez dans un jeu video que cette fonction getColor soit executée 500 fois par seconde…. Comme nous avons synchronisé toute la méthode, l’accès pour implement lire la valeur de cet attribut est alors très très lent. Environ 105 fois plus lent. D’autre part comme l’object est déjà initialisé, la synchronisation ne sert plus à rien.
La solution qui peut résoudre cela serait donc de ne se synchroniser que lors de la création de l’attribut color. C’est ce que l’on a appelé Double-checked locking et qui semblait pendant un temps être une solution pour résoudre ce problème.
Pour revenir à notre class Monstre, si j’applique tel quel ce système voici le code:
/** * Step 2 * Returns the color attribute. * @return a safe copy of the color attribute. */ public Color getColor(){ if(color==null){ synchronized(this) { if(color==null){ if(size<50) { color=Color.RED; }else{ color=Color.BLUE; } } } } return color; }
Voici l’explication: soit 2 threads exécutant la fonction getColor en même temps. La première vérifie que color est null, ce qui est très rapide, puis ensuite tente de prendre un verrou sur l’instance de l’objet. Une autre thread s’exécute en même temps. Elle aussi voit que l’attribut color est null, elle va donc tenter de prendre le verrou. Comme la Thread 1 a déjà celui-ci, la Thread 2 attend. La Thread 1 reverifie que color est toujours null, c’est le deuxième if dans le bloc synchronisé. Ensuite, puisque pour l’instant c’est vrai, elle procède à l’initialisation et donc color ne sera plus null. Enfin elle relâche le verrou et retourne color. La Thread 2 qui était en attente est alors réveillée. Elle entre dans le bloc synchronisé. Cet fois-ci, comme color n’est plus null, elle va directement ressortir et donc il n’y aura pas 2 fois la même instantiation.
Jusqu’ici tout va bien pensez-vous… Erreur, cette solution ne marche pas dans tous les cas.
Pendant quelques temps, cette solution du double verrouillage pour pouvoir retarder l’initialisation semblait être la solution… jusqu’à ce que l’on prenne en compte que l’affectation d’un object en Java n’est pas atomique. D’autre part, si le compilateur optimise le byte code ou sinon ma class Monster est exécutée sur une machine à plusieurs processeurs utilisant de la mémoire partagée, cela ne marchera pas. Ainsi parfois la Thread 2 recommence à s’exécuter alors que l’affectation de color n’est pas encore effectuée par la Thread 1. La question est comment Java peut se permettre de changer l’ordre d’execution ?
Dans les spécifications du modèle de mémoire de Java, il est écrit clairement que les compilateurs et les machines virtuelles Java sont autorisées à optimiser l’ordre du bytecode, tant que le résultat final de l’exécution n’est pas faussé. Ce qui veut dire que la fonction getColor retournera toujours une valeur correcte, mais peut-être que l’initialisation de l’objet a été effectué 2 fois. C’est un peu complexe à expliquer ici en quelques mots, et je vous propose de lire cet article en anglais qui en dit plus et surtout cet article assez connu the « Double-Checked Locking is Broken » Declaration dont les auteurs sont David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer.
Faut-il jeter ce système à la poubelle ? Bien sûr que non. Peut-être que vous n’avez pas plusieurs threads et que dans ce cas, ce modèle est valide. Il faut savoir aussi que dans le cas où vous effectuez des initialisations de type simple comme int ou float, le système de la double synchronisation fonctionne. Il ne marche pas pour les objets complexes, ni pour les types java codés sur 64 bits comme double. Enfin grâce à Java il existe une solution basée sur l’utilisation de ThreadLocal et aussi des solutions avec l’opérateur volatile
. Il faut alors utiliser une JVM 1.4 ou 5 car les versions antérieures sont trop lentes.