nov.
2008
Java Quiz #22
Au programme cette semaine, un peu de synchronisation...
Imaginez que dans une classe, vous avez besoin de faire des initialisations, mais que vous ne voulez faire ces initialisations qu'au premier appel.
Vous écrivez alors ceci :
private static Boolean inited = Boolean.FALSE; ... public void doJob() { // On commence par initialiser ce qui doit l'être synchronized (inited) { if (!inited) { init(); inited = Boolean.TRUE; } } // On fait le boulot ... } ...
Or ce code peut poser problème. Pourquoi ?
Réponse : le problème vient du fait que l'on fait un synchronized
sur une instance de java.lang.Boolean
.
Si cette classe est la seule à faire un tel synchronized
, aucun problème ne devrait se manifester. Par contre, si jamais une autre classe fait aussi un synchronized
sur une instance de java.lang.Boolean
, alors il y a un risque élevé de deadlock, parce que les objets Boolean.FALSE
et Boolean.TRUE
sont partagés pour toute la JVM.
Moralité :
1 - ne jamais poser de verrou sur une variable que l'on modifie dans le bloc synchronisé...
2 - ne jamais poser de verrou sur une variable que l'on n'a pas soi-même instanciée. Ou plus exactement, il faut se méfier des diverses optimisations du langage, qui rendent communes à toute la JVM certaines valeurs :
- celles de
Boolean
, - celles des classes wrapper (
Integer
,Byte
...) représentant des nombres inférieurs à 128, - les chaînes de caractère qui sont placées dans le pool interne
- etc...
Commentaires
Vite fait, je dirais que le compilo peut optimiser le code en déplaçant le 'if' en dehors du synchronized. Un peu comme le problème du double checked locking. M'enfin, je dis ça, j'dis rien.
Tom
Je dirai déjà que le "if(!inited)" est incorrect et devrait être "if(!inited.booleanValue())". Ensuite, je pense que le problème vient de la synchronisation sur "inited" seulement qui ne doit pas permettre à l'"init()" d'être thread-safe.
Pour Tom, le compilateur ne peut (normalement) pas réaliser cette opération, du fait du synchronized. Exemple de code qui change tout : 1. if(!inited.booleanValue()) { 2. synchronized(inited) { 3. // something... 4. inited = Boolean.TRUE; 5. } 6. }
Imagine deux threads A et B. A arrive en 1, remarque que le inited est à FALSE, donc entre dans le "if" puis passe la main à B. B arrive en 1, remarque que le inited est à FALSE (pas encore changé donc c'est bon), donc entre dans le "if" aussi. Dès lors, quelle que soit la façon qu'ils ont de se repasser la main, on fera deux fois le "// something...".
La synchro sur une variabLe non finale et static ne marche pas ? Nicolas
inited non volatile, non ?
Si :
- un thread A est dans l'init,
- un thread B est au synchronized(inited), l'optimiseur place inited dans un registre du processeur,
- A passe inited à true, relâche le lock ce qui libère B,
- B arrive donc au if(!inited) mais sans relire la nouvelle valeur d'inited et donc re-execute l'init.
Y'a de l'idée :) Mais ça peut être encore plus grave que ça.
Plus grave ? Un thread bloqué sur le lock alors :) ?
A Bh@Mp0
Le "
if (!inited)
" est tout à fait correct avec Java5, car le compilateur (unboxing oblige) le transforme derechef en "if (!inited.booleanValue())
".A Tom
Si je comprends bien ce que tu proposes, c'est ni plus ni moins qu'un simple-check locking. Déjà que le double check-locking est un anti-pattern (parce que subtilement buggué sur les plate-formes modernes), mais alors un simple-check locking, c'est le bug garanti... Mauvaise idée ! :-)
A Pascal
inited
est non volatile, et il n'a pas besoin de l'être : lesynchronized
se fait surinited
, ce qui garantit que le thread B relit la valeur des variables qui ont été modifiées par un autre thread (dontinited
) lorsqu'il rentre dans ce blocsynchronized
après avoir acquis le verrou associé (cf. JLS (17)).Ne serait-ce pas une histoire d'immuabilité ? Si Boolean.TRUE est immuable, tous les appels à Boolean.TRUE seraient synchronisé dans ce cas ?