Il était une fois le Singleton en Java
Parmi tous les Design Patterns, le plus connu est sûrement le Singleton. Malheureusement, son implémentation n’est pas aussi triviale qu’il n’y parait en Java. Voici donc quelques exemples d’implémentation…
Sommaire
Eager initialization
Si l’application a toujours besoin de cette instance et que sa création ne mobilise pas trop de ressource, il est acceptable de l’écrire ainsi :
public class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton() {} public static EagerSingleton getInstance() { return instance; } }
Cependant, il est souvent nécessaire de retarder le chargement de l’instance. De plus, lors de l’implémentation d’un composant réutilisable, il ne faut pas imposer le chargement de tous les singletons à l’application utilisatrice si elle n’en a pas besoin !
Lazy initialization
C’est pourquoi la première implémentation réfléchie mais naïve qui vient à l’esprit est la suivante :
public class LazyInitializationSingleton { private static LazyInitializationSingleton instance; private LazyInitializationSingleton(){} public static LazyInitializationSingleton getInstance() { if (instance==null) { instance = new LazyInitializationSingleton(); } return instance; } }
Elle semble juste. Cependant, elle n’est pas thread-safe. Dans un contexte multi-thread, plusieurs instances peuvent être créées.
Double check thread-safe
Une fois que la problématique multi-thread est prise en compte, le développeur est tenté de l’implémenter de cette manière :
public class ThreadSafeSingleton { private static ThreadSafeSingleton instance; private ThreadSafeSingleton() {} public static ThreadSafeSingleton getInstance() { if (instance==null) { synchronized (ThreadSafeSingleton.class) { if (instance==null) { instance = new ThreadSafeSingleton(); } } } return instance; } }
Cette implémentation donne l’impression de fonctionner. Or selon, le compilateur, la JVM ou l’OS, le comportement peut différer. A tel point que le « double check locking » est considéré comme un antipattern. Java 5.0 étend la sémantique du mot-clé volatile pour garantir le « flush » des caches avant l’usage de la variable. En utilisant cette solution, vous acceptez d’avoir un code moins performant.
L’énumération
Dans la seconde édition d’Effective Java, Joshua Bloch propose d’utiliser les [i]enum[/i] pour implémenter un singleton :
public enum EnumSingleton { INSTANCE; public static void doStuff() { // do stuff } }
Cette solution est terriblement simple mais possède un inconvénient majeur : les énumérations sont moins flexibles que les classes (héritage, sérialisation, etc…).
Bill Pugh Singleton
Cette implémentation tente de réconcilier toutes précédentes au moyen d’une classe imbriquée :
public class BillPughSingleton { private BillPughSingleton() {} static private class SingletonHelper { private static final BillPughSingleton INSTANCE = new BillPughSingleton(); } public static BillPughSingleton getInstance() { return SingletonHelper.INSTANCE; } }
Elle reprend le principe de la [i]Eager initialization[/i] sans avoir l’inconvénient identifié sur le [i]lazy-loading[/i] puisque la classe imbriquée n’est connue de personne. Enfin, cette implémentation est thread-safe sans avoir à recourir à des mots-clés du langage tels que [i]synchronized[/i], [i]volatile[/i] ou [i]enum[/i].
Avant de terminer, sachez que si vous utilisez des frameworks d’injection de dépendances tel que Spring Framework, il est possible de le laisser gérer vos classes comme des singletons.
Pourquoi vous avoir présenté toutes ces implémentations ? Simplement pour montrer que l’implémentation d’un Design Pattern, aussi simple soit-il, n’est pas aussi triviale qu’il n’y parait. Et la première question à se poser est : « Pourquoi utilise-t-on ce modèle de conception ? ». Il est également recommandé de bien documenter l’implémentation afin d’expliciter le contexte d’utilisation. 😉
Histoire à suivre…
https://blog.lecacheur.com/2013/10/28/il-etait-une-fois-le-singleton-en-java/ConceptionJava EEconception,design pattern,singletonParmi tous les Design Patterns, le plus connu est sûrement le Singleton. Malheureusement, son implémentation n'est pas aussi triviale qu'il n'y parait en Java. Voici donc quelques exemples d'implémentation... Eager initialization Si l'application a toujours besoin de cette instance et que sa création ne mobilise pas trop de ressource, il est...SeBSébastien LECACHEUR23r0@laposte.netAdministratorLe weblogue de SeB
Désolé m’sieur, mais le « Double check test » n’a JAMAIS été thread-safe, pas plus avec du JDK 1.4 qu’avec du 1.5. C’est simplement l’apparition des processeurs multi-coeurs qui ont permis à pas mal de développeurs de s’apercevoir qu’ils faisaient une énorme connerie en pensant que les transferts mémoire au niveau des caches s’exécutaient nécessairement dans l’ordre ou il étaient écrits dans le code source.
Autant que je me souvienne, même en JDK 1.2, on trouvait déjà des bugs issus de cette fausse bonne idée. La propension à la manifestation du phénomène dépend à la fois du hardware de l’implémentation de la JVM utilisée.
Effectivement, quand j’ai commencé cette note, pour moi le problème du « double check » venait du cache et avait été mis en exergue avec l’apparition des processeurs multi-coeurs. Mais en me redocumentant et ne trouvant pas les articles que j’avais pu lire à l’origine, j’ai fait quelques raccourcis maladroits et me suis moi-même embrouillé. :-/ Merci pour ta rectification !
Que penses-tu si je remplace le paragraphe erroné par celui-ci :
« Cette implémentation donne l’impression de fonctionner. Or selon, le compilateur, la JVM ou l’OS, le comportement peut différer. A tel point que le « double check locking » est considéré comme un antipattern. Java 5.0 introduit le mot-clé volatile pour résoudre ce problème en acceptant de perdre en terme de performance. »
Je peux garder cette référence :http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html et peut-être ajouter celle-ci : http://fr.wikipedia.org/wiki/Double-checked_locking
Merci merci pour ta remarque pertinente !
La correction que tu propose me paraît tout à fait appropriée.
J’enlèverais juste ce que tu dis sur l’introduction du mot clé volatile car il n’a pas été introduit par java 5.0 mais a toujours existé (même en java 1.0).
Pour l’utilisation de « volatile », j’attirerais tout de même l’attention sur le faît que ça ne fonctionne qu’avec un JDK 5.0 ou plus car la sémantique du mot clé a été étendue uniquement à partir de cette version pour garantir le « flush » des caches (une barrière mémoire) avant usage de la variable (lecture ou écriture).
Je viens de mettre à jour la note. Encore merci pour tes remarques et suggestions de correction !
Après discussion, je vais enlever la remarque sur le lazy-loading avec les enums. Il est respecté dans le cas général.
On pourrait juste reprocher qu’il n’est pas maitrisé. 🙂
comme l’implémentation Bill Pugh Singleton est considérée la meilleure, peut-on l’utiliser tjrs, ou y-a-il des limites ?
J’avoue être un peu rouillé sur l’implémentation de singleton. N’ayant pas eu besoin de le faire depuis un long moment, je me demanderais dans un premier temps s’il y a vraiment besoin d’en implémenter un…
Bonjour,
le classloader ne charge une classe qu’au moment où elle commence à être utilisée par une autre ByteCode.
Ainsi une classe ou instance n’est JAMAIS EAGER, sauf si on l’appelle explicitement, avec un Class.forName(…) par exemple.
Faites, le test vous verrez qu’un Singleton ne se charge pas tout seul …
Il faut une ligne de code qui l’appelle ou qui y fasse référence. Tant que ce n’est pas le cas, et même si le programme tourne, le singleton n’est pas en mémoire.