Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Double-check Locking

7 réponses
Avatar
Mike Baroukh
Bonjour.

J'utilise beaucoup le "Double-check Locking". Du moins, j'ai appris
aprés que ça s'appelait comme ça.
Hors, récemment, j'ai lu des articles indiquant que ca ne fonctionnait pas :
http://www.javaworld.com/jw-02-2001/jw-0209-double.html
ou
http://faculty.washington.edu/stepp/courses/2005winter/tcss360/readings/12a-singleton.html

Il y a certaines choses que je n'ai pas compris à ce sujet, et je me
permet donc de partager ma reflexion avec ce groupe de discussion. Si
quelqu'un

Voici un code de "double-check locking" :

private static Value value = null;
public static Value getValue() {
if (value==null) {
synchronized(Value.class) {
if (value==null) {
value = new Value();
}
}
}
return value;
}

Objectif : initialiser value lors du premier appel, en évitant les
synchronisations pour les appels suivants.
Fonctionnement : Le premier thread va aller directement au new Value().
Si un ou +sieurs autres threads arrivent au même moment, ils sont
bloqués sur la synchronisation.
Dés que le premier thread est est sorti du bloc synchronisé, les autres
thread passent 1 par 1 sur la ligne du "if (value==null)" et
n'exécuterons donc pas la création de l'instance.

Tous les autres threads qui arrivent après que l'instance a été créé,
n'exécuterons même pas la synchronisation.

Ceci évite de mettre l'intégralité de la méthode en synchronisé.

Bien sur, ce cas est très simple et aurait pu être résolu par un
private static Value value = new Value();
mais c'est juste pour l'exemple.

Le problème : Du au modèle mémoire Java, l'opération
value = new Value();
n'est jamais garantie d'être exécutée dans un ordre cohérent.
C'est à dire :
Normalement, ça devrait être
réserver l'espace mémoire, initialiser l'objet, appeler le constructeur,
affecter la référence.
Hors, Java autorise à ce que la JVM fasse, par exemple :
reserver l'espace memoire, affecter la référence, initialiser l'objet,
appeler le constructeur.

Donc, dans ce deuxième cas, un thread qui entre dans la méthode juste
aprés l'affectation de la référence, va considérer que l'objet est déjà
présent (var value!=null) et le retourner. Hors, en fait, l'appel du
constructeur n'ayant pas été encore réalisé, un thread va commencer à
travailler avec cette instance AVANT qu'elle soit initialisée.

Comment je m'en sert :

Je me sers énormément de ce style de code.
La façon dont je l'utilise en général est la suivante :

public static MonObject getMonObjet(Object key) {
MonObjet retour = MonCache.get(key);
if (retour==null) {
synchronized(MonObjet.class) {
retour = MonCache.get(key);
if (retour==null) {
retour = *rechercher l'objet en base de données*;
MonCache.put(key, retour);
}
}
}
return retour;
}

Mon problème :

Je ne sais pas si j'en ai un (de problème).
Je n'arrive pas à savoir si je rentre dans le cas de figure.
Par exemple, QUAND est terminée l'initialisation de l'objet "retour" ?
A la sortie de la méthode ? lorsque je le mets dans le cache ?

Pour aller plus loin, si l'initalisation est forcée lorsque je passe
l'objet au cache, n'y aurait-il pas une parade au problème qui
ressemblerait à ceci :


private Object forceFlush(Object o) {
return o;
}

private static Value value = null;
public static Value getValue() {
if (value==null) {
synchronized(Value.class) {
if (value==null) {
Value tmp = (Value)forceFlush(new Value());
value = tmp;
}
}
}
return value;
}


Merci d'avance pour vos éclaircissements éventuels ...
(et félicitation pour m'avoir lu jusqu'au bout !)


Mike

7 réponses

Avatar
Insitu
Mike Baroukh writes:

Bonjour.


Je me sers énormément de ce style de code.
La façon dont je l'utilise en général est la suivante :

public static MonObject getMonObjet(Object key) {
MonObjet retour = MonCache.get(key);
if (retour==null) {
synchronized(MonObjet.class) {
retour = MonCache.get(key);
if (retour==null) {
retour = *rechercher l'objet en base de données*;
MonCache.put(key, retour);
}
}
}
return retour;
}

Mon problème :

Je ne sais pas si j'en ai un (de problème).
Je n'arrive pas à savoir si je rentre dans le cas de figure.
Par exemple, QUAND est terminée l'initialisation de l'objet "retour" ?
A la sortie de la méthode ? lorsque je le mets dans le cache ?


Je pense que dans ce cas, il n'y en a pas de probléme. La question à
se poser es t comment cela se traduit dans la jvm. Dans le cas de
l'appel direct au constructeur, voici ce que cela donne normalement:


|
private Toto toto; | 1: new Toto // met un objet toto sur la pile
... | 2: dup
| 3: invokespecial init
toto = new Toto() | 4: putfield toto
|

MAIS le compilateur peut aussi faire:

|
private Toto toto; | 1: new Toto // met un objet toto sur la pile
... | 2: dup
| 3: putfield toto
toto = new Toto() | 4: invokespecial init
|

ce qui provoque le probléme...

Si tu passe par une variable secondaire, l'affectation devient
atomique et l'objet est initialisé correctement ou vaut null.

PS tu peux vérifier le résultat de la compilation par
javap -c MaClasse


insitu.

Avatar
Insitu
writes:


Pourtant, le passage par une variable intermédiaire pose semble t-il
probleme comme indiqué au paragraphe
"A fix that doesn't work"
sur
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html



Le code n'est pas identique, il y a un bloc synchronisé en plus qui ne
sert pas à vérouiller l'accés mais à construire (du moins en théorie) une
barrière mémoire. Cependant, l'article précise bien que *toutes* les
solutions qui n'utilisent pas de barrière mémoire explicite sont
incorrectes ou au moins ne couvrent pas toutes les implantations
possibles.

Ce n'est pas la première fois que je rencontre le Double-checked
locking et je me fais toujours prendre...

insitu.

Avatar
Alain

Bonjour.

J'utilise beaucoup le "Double-check Locking". Du moins, j'ai appris
aprés que ça s'appelait comme ça.
grosso modo c'est pas compliqué...

aucune solution complexe ne marche, et toute peuvent créer une bug.

il n'y a que deux choses qui marchent :

- initialiser dans le constructeur de classe (membre statique, ou
section d'init de classe)

static C singleton=new C();
static C getInstance() {return singleton;}
ou si le constructeur suffit pas...
class Singleton {
static C singleton;
static C getInstance() {return singleton;}
static {
singleton=new C()
singleton.init();
}
}

- dans une méthode synchronisée ou un bloc synchronisé.
static C singleton;
static synchronized C getInstance() {
if(singleton==null) singleton=new C();
return singleton;
}

la première solution utilise le fait qu'une classe n'est chargée que
lorsque qu'on l'utilise la première fois... et que c'est synchronisé.

la deuxième solution marche bien, et si certains l'ont éliminé c'est a
cause de problème de lenteur de la synchronisation à très forte charge
multi-thread (ex: dans un serveur J2EE très chargé) pour des singletons
utilisés très très fréquemment...
la JVM a je crois gagné en performance de ce coté, et de toute façon il
n'y a pas beaucoup d'objets singleton qui soit utilisé aussi souvent.

bref comme d'habitude, à vouloir gagner inutilement des performances, on
crée des bugs réels.

sinon peut être y a t'il quelque chose avec les ThreadLocal, mais je
crains que ca ne soit pas plus rapide et que la synchro soit la même en
plus complexe.

à bien y réfléchir, la première solution devrait toujours convenir.
on pinaille pour rien.

Avatar
Laurent Desgrange
Alain wrote:
grosso modo c'est pas compliqué...
aucune solution complexe ne marche, et toute peuvent créer une bug.

il n'y a que deux choses qui marchent :

- initialiser dans le constructeur de classe (membre statique, ou
section d'init de classe)

static C singleton=new C();
static C getInstance() {return singleton;}
ou si le constructeur suffit pas...
class Singleton {
static C singleton;
static C getInstance() {return singleton;}
static {
singleton=new C()
singleton.init();
}
}

la première solution utilise le fait qu'une classe n'est chargée que
lorsque qu'on l'utilise la première fois... et que c'est synchronisé.

à bien y réfléchir, la première solution devrait toujours convenir.
on pinaille pour rien.


Je n'en suis pas certain, par contre je n'ai pas de démonstration. En
revanche j'ai travaillé sur un projet (JEE) utilisant des singletons
initialisés via double-check locking et je n'avais pas de problèmes. Un
jour je suis tombé sur l'exemple que vous venez de donner, et comme c'était
autrement plus simple et plus court à écrire, j'ai remplacé dans mes
singletons.
Après cette modification l'application ne fonctionnait plus... des problèmes
lors de l'initialisation des singletons (je ne me souviens plus quel était
le problème exactement). Bref, en environnement multi-threadé sur une JVM
1.5 cette solution ne m'a donc pas semblée top, seul le bon vieux double
check locking avec une instance volatile semble fonctionner à tous les
coups.

Avatar
Alain
le problème des bug du double check locking est que ca peux planter sur
une implémentation de JVM, quand le code est optimisé d'une certaine
manière... ce qui peut arriver si le singleton est appelé souvent.

quand a la méthode de l'initialisation par la classe, il faut
penser a appeler les initialiseur (soit dans le constructeur, soit
dans un initialiseur de classe)


vu la simplicité du système, ya pas grand chose qui peut merder.

sinon reste la méthode synchronized, ce qui doit toujours marcher, à
quelque nanosecondes près



la première solution utilise le fait qu'une classe n'est chargée que
lorsque qu'on l'utilise la première fois... et que c'est synchronisé.

à bien y réfléchir, la première solution devrait toujours convenir.
on pinaille pour rien.


Je n'en suis pas certain, par contre je n'ai pas de démonstration. En
revanche j'ai travaillé sur un projet (JEE) utilisant des singletons
initialisés via double-check locking et je n'avais pas de problèmes. Un
jour je suis tombé sur l'exemple que vous venez de donner, et comme c'était
autrement plus simple et plus court à écrire, j'ai remplacé dans mes
singletons.
Après cette modification l'application ne fonctionnait plus... des problèmes
lors de l'initialisation des singletons (je ne me souviens plus quel était
le problème exactement). Bref, en environnement multi-threadé sur une JVM
1.5 cette solution ne m'a donc pas semblée top, seul le bon vieux double
check locking avec une instance volatile semble fonctionner à tous les
coups.




Avatar
Mike Baroukh
à bien y réfléchir, la première solution devrait toujours convenir.
on pinaille pour rien.



Dans le cas de la création d'un singleton, peut-être.
Mais ce n'est pas utilisé systématiquement dans ce cas.

Exemple : je souhaite gérer un cache qui, en fonction d'une clée,
renvoit un objet plutôt couteux à créer en terme de performances.
L'idée est donc de le cacher dans un map.
Mais ce que je ne souhaite pas c'est, en cas de demande importante du
même objet, le calculer plusieurs fois.
(finalement, c'est un genre de singleton ...).

Donc,
- je ne peux pas le créer en statique (puisqu'il dépend de la clée)
- si je synchronise toute la méthode je risque d'avoir une accumulation
de demandes pour un objet justement long à calculer ...

le double-check locking est à priori la seule solution que je vois ...

Mike




Bonjour.

J'utilise beaucoup le "Double-check Locking". Du moins, j'ai appris
aprés que ça s'appelait comme ça.
grosso modo c'est pas compliqué...

aucune solution complexe ne marche, et toute peuvent créer une bug.

il n'y a que deux choses qui marchent :

- initialiser dans le constructeur de classe (membre statique, ou
section d'init de classe)

static C singleton=new C();
static C getInstance() {return singleton;}
ou si le constructeur suffit pas...
class Singleton {
static C singleton;
static C getInstance() {return singleton;}
static {
singleton=new C()
singleton.init();
}
}

- dans une méthode synchronisée ou un bloc synchronisé.
static C singleton;
static synchronized C getInstance() {
if(singleton==null) singleton=new C();
return singleton;
}

la première solution utilise le fait qu'une classe n'est chargée que
lorsque qu'on l'utilise la première fois... et que c'est synchronisé.

la deuxième solution marche bien, et si certains l'ont éliminé c'est a
cause de problème de lenteur de la synchronisation à très forte charge
multi-thread (ex: dans un serveur J2EE très chargé) pour des singletons
utilisés très très fréquemment...
la JVM a je crois gagné en performance de ce coté, et de toute façon il
n'y a pas beaucoup d'objets singleton qui soit utilisé aussi souvent.

bref comme d'habitude, à vouloir gagner inutilement des performances, on
crée des bugs réels.

sinon peut être y a t'il quelque chose avec les ThreadLocal, mais je
crains que ca ne soit pas plus rapide et que la synchro soit la même en
plus complexe.

à bien y réfléchir, la première solution devrait toujours convenir.
on pinaille pour rien.



Avatar
Alain
Dans le cas de la création d'un singleton, peut-être.
Mais ce n'est pas utilisé systématiquement dans ce cas.

Exemple : je souhaite gérer un cache qui, en fonction d'une clée,
renvoit un objet plutôt couteux à créer en terme de performances.
L'idée est donc de le cacher dans un map.
Mais ce que je ne souhaite pas c'est, en cas de demande importante du
même objet, le calculer plusieurs fois.
(finalement, c'est un genre de singleton ...).
effectivement, la création a l'initialisation de classe suppose que la

classe est créée à la demande, mais là on plusieurs objets a créer après.



Donc,
- je ne peux pas le créer en statique (puisqu'il dépend de la clée)
oui effectivement.


- si je synchronise toute la méthode je risque d'avoir une accumulation
de demandes pour un objet justement long à calculer ...
en fait c'est ce qu'il faut.

si plusieurs demande d'accès arrivent pendant la création, elle doivent
être mise en attente de la vrai création.

de toute façon les thread concurrentes qui demandent l'accès, attendrons
pas plus que le temps de la création d'un objet, puis que en fait ils
vont attendre la création , par la première thread, de l'objet,
opération qui est déjà commencée.

le pire ce serait de créer plusieurs instances en parallèle, distinctes,
et en prenant plus de temps.

par contre, et c'est peut être l'objet de ton scepticisme, il faudrait
non pas verrouiller la méthode getInstance(clé), nu même la Map entière,
mais faire un verrouillage à deux niveau (je l'ai déjà codé).
1-verrouiller la Map
2- récupérer l'élément dans la map,

cas A: si absent
3a- le créer (rapidement, donc vide)
4a- verrouiller l'élément
5a- déverrouiller la Map
6a- l'initialiser


7- déverrouiller l'Entry


et là c'est le drame, parce que pour coder le verrouillage et
déverrouilage en java, avec les synchronize, wait et notify, c'est
western. je l'ai fait une fois, mais j'ai plus le code.

je crois qu'il faudrait mettre dans la Map, non pas un objet, mais un
"Holder" qui aura une "value" qui sera le vrai objet, tandis que le
Holder sera synchronisé, et ensuite lu ou remplis.


en JDK1.5
ca devrait pas être sorcier avec
java.util.concurrent.Semaphore
et java.util.concurrent.ConcurrentMap

si quelqu'un peut retrouver le code.
ca doit être un truc standard, à pas réinventer



le double-check locking est à priori la seule solution que je vois ...


ca ne change rien, le double check locking ne marche pas sur certaines
machines, point... et en plus il ne fait que prétendument accélérer un
synchonize, pas en changer la logique (attendre le temps que l'init se
fasse).

le problème c'est que le double check locking est basé sur l'idée qu'en
faisant un test sans synchronize avant on détecte quand le pointeur est
non null depuis longtemps, pas forcément si c'est récent, mais qu'on ne
vois jamais de non null avant que ce soit initialisé...

en fait ce que l'article cité au premier poste de la série, montre c'est
qu'on peut avoir un pointeur affecté, sans que l'objet n'ait été
initialisé, et donc le retourner avant sa construction.
évidemment c'est lié à des situation d'optimisation violente et
imprévisible par le compilateur hotspot.

en plus c'est plus compliqué, et vu que ici c'est déjà compliqué...