Je suis tombé sur un article récemment consacré à la configuration de verrouillage vérifiée sous Java et à ses pièges. Je me demande maintenant si une variante de cette configuration que j'utilise depuis des années est susceptible de poser problème.
J'ai consulté de nombreux articles et articles sur le sujet et compris les problèmes potentiels liés à l'obtention d'une référence à un objet partiellement construit. Autant que je sache, je n'ai pas pense ma mise en œuvre est soumise à ces questions. Existe-t-il des problèmes avec le modèle suivant?
Et si non, pourquoi les gens ne l'utilisent-ils pas? Je n'ai jamais vu cela recommandé dans aucune des discussions que j'ai vues autour de cette question.
public class Test {
private static Test instance;
private static boolean initialized = false;
public static Test getInstance() {
if (!initialized) {
synchronized (Test.class) {
if (!initialized) {
instance = new Test();
initialized = true;
}
}
}
return instance;
}
}
Le double contrôle est cassé . Comme initialisé est une primitive, il n'est pas forcément nécessaire qu'elle soit volatile pour fonctionner. Toutefois, rien n'empêche que initialized soit considéré comme conforme au code non syncronisé avant que l'instance ne soit initialisée.
EDIT: Pour clarifier la réponse ci-dessus, la question initiale portait sur l’utilisation d’un booléen pour contrôler le verrouillage en double contrôle. Sans les solutions dans le lien ci-dessus, cela ne fonctionnera pas. Vous pouvez revérifier le verrouillage en définissant un booléen, mais vous avez toujours des problèmes de réorganisation des instructions lors de la création de l'instance de la classe. La solution suggérée ne fonctionne pas car l'instance peut ne pas être initialisée après que vous voyez le booléen initialisé comme étant vrai dans le bloc non synchronisé.
La bonne solution pour vérifier le verrouillage consiste à utiliser volatile (sur le champ de l’instance) et à oublier le booléen initialisé, et à utiliser JDK 1.5 ou une version supérieure, ou à l’initialiser dans un champ final, comme indiqué dans le lien. article et la réponse de Tom, ou tout simplement ne l'utilisez pas.
Certes, le concept dans son ensemble semble être une énorme optimisation prématurée, à moins que vous ne sachiez que vous allez vous disputer avec une tonne de discussions pour obtenir ce Singleton, ou que vous avez profilé l’application et que vous l’avez vu comme un point chaud.
Cela fonctionnerait si initialized
était volatile
. Tout comme avec synchronized
, les effets intéressants de volatile
ne concernent pas tant la référence que ce que nous pouvons dire sur d’autres données. La configuration du champ instance
et de l'objet Test
est forcée de arrive-avant l'écriture dans initialized
. Lorsque vous utilisez la valeur mise en cache via le court-circuit, la variable initialize
lit se produit-avant - lit la valeur instance
et les objets atteints par la référence. Il n'y a pas de différence significative dans le fait d'avoir un indicateur initialized
séparé (à part que cela provoque encore plus de complexité dans le code).
(Les règles pour les champs final
dans les constructeurs pour la publication non sécurisée sont un peu différentes.)
Cependant, vous devriez rarement voir le bogue dans ce cas. Les risques de problèmes lors de la première utilisation sont minimes et il s’agit d’une course non répétée.
Le code est trop compliqué. Vous pouvez simplement l'écrire comme ceci:
private static final Test instance = new Test();
public static Test getInstance() {
return instance;
}
Le double contrôle est en effet cassé, et la solution au problème est en réalité plus simple à implémenter en termes de code que cet idiome - utilisez simplement un initialiseur statique.
public class Test {
private static final Test instance = createInstance();
private static Test createInstance() {
// construction logic goes here...
return new Test();
}
public static Test getInstance() {
return instance;
}
}
Un initialiseur statique est garanti pour être exécuté la première fois que la machine virtuelle Java charge la classe et avant que la référence de classe ne puisse être renvoyée à un thread, ce qui la rend intrinsèquement threadsafe.
C'est la raison pour laquelle le double contrôle est cassé.
Synchronize garantit qu'un seul thread peut entrer un bloc de code. Mais cela ne garantit pas que les modifications de variables effectuées dans la section synchronisée seront visibles par les autres threads. Seuls les threads qui entrent dans le bloc synchronisé sont assurés de voir les modifications. C'est la raison pour laquelle le double contrôle est rompu - il n'est pas synchronisé du côté du lecteur. Le fil de lecture peut voir que le singleton n'est pas null, mais que les données de singleton peuvent ne pas être complètement initialisées (visibles).
La commande est fournie par volatile
. volatile
garantit l'ordre, par exemple, l'écriture sur un champ statique singleton volatil garantit que l'écriture sur l'objet singleton sera terminée avant l'écriture sur un champ statique volatile. Cela n'empêche pas la création d'un singleton de deux objets, cela est fourni par synchronize.
Les champs statiques finaux de classe n'ont pas besoin d'être volatils. En Java, le JVM s’occupe de ce problème.
Voir mon post, une réponse à Motif singleton et double verrouillage brisé dans une application Java réelle , illustrant un exemple de singleton en ce qui concerne le verrouillage à double contrôle qui a l'air intelligent, mais reste cassé.
Double vérification du verrouillage est l'anti-motif.
Classe de titulaire d'initialisation paresseux est le motif que vous devriez rechercher.
En dépit de nombreuses autres réponses, j’ai pensé que je devrais y répondre, car il n’existait pas encore de réponse simple indiquant pourquoi DCL était brisé dans de nombreux contextes, pourquoi il était inutile et ce que vous devriez faire à la place. Je vais donc utiliser une citation de Goetz: Java Concurrency In Practice
qui, pour moi, fournit l'explication la plus succincte dans son dernier chapitre sur le modèle de mémoire Java.
Il s’agit de la publication sécurisée de variables:
Le vrai problème avec DCL est l’hypothèse que la pire chose qui puisse arriver lors de la lecture d’une référence d’objet partagé sans synchronisation est de voir par erreur une valeur périmée (dans ce cas, null); dans ce cas, le langage DCL compense ce risque en essayant de nouveau avec le verrou en place. Mais le pire des cas est considérablement pire: il est possible de voir une valeur actuelle de la référence mais des valeurs périmées pour l'état de l'objet, ce qui signifie que l'objet peut être considéré comme étant dans un état non valide ou incorrect.
Les modifications ultérieures dans JMM (Java 5.0 et versions ultérieures) ont permis à DCL de fonctionner si les ressources sont rendues volatiles, ce qui a un impact limité sur les performances, car les lectures volatiles ne sont généralement qu'un peu plus chères que les lectures non volatiles.
Cependant, il s’agit d’un idiome dont l’utilité est largement révolue: les forces qui l’ont motivé (synchronisation lente et non contrôlée, démarrage lent de la machine virtuelle) ne sont plus en jeu, ce qui la rend moins efficace en tant qu’optimisation. L'idiome de titulaire d'initialisation paresseux offre les mêmes avantages et est plus facile à comprendre.
Listing 16.6. Idiome de classe de détenteur pour l'initialisation paresseuse.
public class ResourceFactory private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } }
C'est la façon de le faire.
Il existe encore des cas où une double vérification peut être utilisée.
final
est défini à la fin du bloc constructeur/initialisé (qui fait en sorte que tous les champs précédemment initialisés soient vus par d'autres threads).Le problème DCL est résolu, même s'il semble fonctionner sur de nombreuses machines virtuelles. Il y a une belle description du problème ici http://www.javaworld.com/article/2075306/Java-concurrency/can-double-checked-locking-be-fixed-.html .
le multithreading et la cohérence de la mémoire sont des sujets plus compliqués qu'ils pourraient paraître. [...] Vous pouvez ignorer toute cette complexité si vous utilisez simplement l'outil fourni par Java dans ce but précis - la synchronisation. Si vous synchronisez chaque accès à une variable qui aurait pu être écrite ou lue par un autre thread, vous n'aurez aucun problème de cohérence de la mémoire.
Le seul moyen de résoudre ce problème correctement consiste à éviter une initialisation paresseuse (faites-le avec impatience) ou à effectuer un contrôle unique dans un bloc synchronisé. L'utilisation de la valeur booléenne initialized
équivaut à une vérification nulle de la référence elle-même. Un deuxième thread peut voir que initialized
est vrai, mais instance
peut encore être null ou partiellement initialisé.
Tout d'abord, pour les singletons, vous pouvez utiliser un Enum, comme expliqué dans cette question Implémenter Singleton avec un Enum (en Java)
Deuxièmement, depuis Java 1.5, vous pouvez utiliser une variable volatile à double contrôle, comme expliqué à la fin de cet article: https://www.cs.umd.edu/~pugh/Java/memoryModel/DoubleCheckedLocking.html
J'ai enquêté sur l'idiome de verrouillage revérifié et, d'après ce que j'ai compris, votre code pourrait poser le problème de la lecture d'une instance partiellement construite SAUF SI votre classe de test est immuable:
Le modèle de mémoire Java offre une garantie particulière de sécurité d'initialisation pour le partage d'objets immuables.
Vous pouvez y accéder en toute sécurité même lorsque la synchronisation n'est pas utilisée pour publier la référence de l'objet.
(Citations du très recommandé livre Java Concurrency in Practice)
Donc, dans ce cas, l'idiome de verrouillage vérifié double fonctionnerait.
Mais si ce n'est pas le cas, observez que vous renvoyez l'instance de variable sans synchronisation, de sorte que la variable d'instance ne soit peut-être pas complètement construite (vous verrez les valeurs par défaut des attributs à la place des valeurs fournies par le constructeur).
La variable booléenne n'ajoute rien pour éviter le problème, car elle peut être définie sur true avant l'initialisation de la classe Test (le mot clé synchronized n'évite pas la réorganisation complète, certaines sencences peuvent modifier l'ordre). Il n'y a pas de règle "arrive avant" dans le modèle de mémoire Java pour le garantir.
Et rendre le volatile booléen n’ajouterait rien non plus, car les variables 32 bits sont créées de manière atomique en Java. Le langage de verrouillage à double vérification fonctionnerait également avec eux.
Depuis Java 5, vous pouvez résoudre ce problème en déclarant la variable d’instance volatile.
Vous pouvez en savoir plus sur l'idiome vérifié double dans cet article très intéressant .
Enfin, quelques recommandations que j'ai lues:
Considérez si vous devriez utiliser le motif singleton. Il est considéré par beaucoup comme un anti-modèle. L'injection de dépendance est préférable dans la mesure du possible. Vérifiez this .
Réfléchissez bien si l'optimisation du double verrouillage est vraiment nécessaire avant de la mettre en œuvre, car dans la plupart des cas, cela ne vaut pas la peine. En outre, envisagez de construire la classe Test dans le champ statique, car le chargement différé n’est utile que lorsque la construction d’une classe prend beaucoup de ressources et dans la plupart des cas, ce n’est pas le cas.
Si vous devez toujours effectuer cette optimisation, cochez link ), qui offre des alternatives pour obtenir un effet similaire à celui que vous essayez.
Si "initialisé" est vrai, alors "instance" DOIT être complètement initialisé, identique à 1 plus 1 égal à 2 :). Par conséquent, le code est correct. L'instance n'est instanciée qu'une seule fois, mais la fonction peut être appelée un million de fois. Elle améliore donc les performances sans vérifier la synchronisation pour un million moins une fois.
Vous devriez probablement utiliser les types de données atomiques dans Java.util.concurrent.atomic .