- Cet article fait partie de la série Démystification du GoF : Les design patterns par la pratique en Java
- Il fait suite à celui-ci : Le pattern Abstract Factory
Le Singleton est le plus « simple » des Design Patterns et surtout le plus connu. Le principe est de n’avoir qu’une seule instance d’une classe Singleton et de renvoyer celle-ci à chaque demande d’instanciation.
Cela peut être particulièrement utilisé notamment pour la gestion de ressources externes comme par exemple la gestion des Log. Une seule instanciation pour gérer les entrées/sorties (classe LOGGER).
Le design pattern Singleton décrit ce concept.
Représentation UML
Cette représentation est une représentation littérale du Singleton. L’implémentation JAVA peut être différente de la représentation UML.
La particularité du Singleton repose dans le fait que son constructeur est privé. Nous ne pouvons pas faire un appel à la classe par :
Singleton my = new Singleton(); // 'Singleton()' has private access in 'Singleton'
Représentation JAVA
Singleton classique
Un singleton minimaliste qui nous permet de récupérer l’instance de cette classe par l’appel à Singleton.getInstance();
public class Singleton {
private static Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
/*Some code here*/
}
Une seule instance sera disponible.
L’inconvénient est dû au chargement de la classe. Quand elle sera chargée dans le ClassLoader de JAVA, la classe sera directement initialisée (allocation mémoire) sans nécessairement vouloir l’utiliser. Pour certains cas où la gestion de la mémoire est primordiale cela peut être gênant.
Singleton en Lazy Loading
Le principe du Lazy Loading corrige le seul « défaut » du singleton classique en n’initialisant pas la variable statique au chargement de la classe mais lors de l’appel à getInstance().
public class LazySingleton {
private static LazySingleton INSTANCE;
private LazySingleton() { }
public static LazySingleton getInstance() {
if (INSTANCE == null)
INSTANCE = new LazySingleton();
return INSTANCE;
}
public void sayHello(String s) {
System.out.println("Class "+s+": " + this);
}
}
La méthode sayHello() ci-dessus nous permettra de voir le hashCode de l’objet JAVA créé. Cela revient à voir l’adresse de l’objet fraichement instancié.
LazySingleton lazy = LazySingleton.getInstance();
lazy.sayHello("LazySingleton");
Output :
Class LazySingleton: LazySingleton@c1e719b
Malheureusement cette implémentation ajoute aussi de gros problèmes…
La concurrence !
Dans un environnement multithread cette implémentation ne gère aucunement l’appel simultané à la méthode getInstance().
/*
* This class show how the concurrency works.
* Implements Runnable interface to be able to call multiple instance
* of our LazySingleton class
*/
public class HelloLazySingleton implements Runnable {
private int cpt;
public HelloLazySingleton(int c) {cpt = c;}
/*
* In each thread, call the getInstance method of LazySingleton class
* Then we call the sayHello class.
*/
public void run() {
LazySingleton s = LazySingleton.getInstance();
s.sayHello("LazySingleton_"+Integer.toString(cpt));
}
/* We're just creating 10 thread to shouw the concurrency*/
public static void main(String args[]) {
for (int i=0;i<10;i++)
(new Thread(new HelloLazySingleton(i))).start();
}
}
Output :
Class LazySingleton_0 : LazySingleton@692e1ca8
Class LazySingleton_9 : LazySingleton@692e1ca8
Class LazySingleton_8 : LazySingleton@692e1ca8
Class LazySingleton_5 : LazySingleton@692e1ca8
Class LazySingleton_1 : LazySingleton@692e1ca8
Class LazySingleton_7 : LazySingleton@692e1ca8
Class LazySingleton_6 : LazySingleton@692e1ca8
Class LazySingleton_4 : LazySingleton@692e1ca8
Class LazySingleton_2 : LazySingleton@692e1ca8
Class LazySingleton_3 : LazySingleton@189e7143
Le thread 3 a un autre hashcode !!!
Nous avons donc 2 objets différents…
Nous ne pouvons donc utiliser cette méthode.
Singleton en Lazy Loading synchronized
Le singleton en Lazy Loading est efficace concernant la mémoire mais niveau concurrence … il perd de son intérêt.
Nous pouvons utiliser le mot clé « synchronized » pour éviter la multiple instanciation.
Synchronized au niveau de la méthode
public class SynchronizedSingleton {
private static SynchronizedSingleton INSTANCE;
private SynchronizedSingleton() {}
/*
* The keyword synchronized
* avoid multiple // call to getInstance method
*/
public synchronized static SynchronizedSingleton getInstance() {
if (INSTANCE == null)
INSTANCE = new SynchronizedSingleton();
return INSTANCE;
}
public void sayHello(String s) {
System.out.println("Class " + s + ": " + this);
}
}
Ok niveau concurrence on est bon. Mais niveau performance …
En cas de multiples appels simultanés à la méthode getInstance(), Java n’autorisera qu’une seule itération à la fois.
Thread 3 rentre dans getInstance()
Thread 2 patiente…
Thread 4 patiente…
—
Thread 3 finit l’appel à getInstance()
Thread 2 patiente…
Thread 4 rentre dans getInstance()
…
Synchronized dans la méthode (Double check locking)
public class SynchronizedDCLSingleton {
private static SynchronizedDCLSingleton INSTANCE;
private SynchronizedDCLSingleton() {}
/*
* First check to INSTANCE variable to get the status
* if null call synchronized to get a lock
* check again the status in the synchronized and if null
* create a new instance.
*/
public static SynchronizedDCLSingleton getInstance() {
if (INSTANCE == null)
synchronized(Singleton.class) {
if (INSTANCE == null)
INSTANCE = new SynchronizedDCLSingleton();
}
return INSTANCE;
}
public void sayHello(String s) {
System.out.println("Class " + s + ": " + this);
}
}
Le « Double-Check Locking » évite le call au synchronized à chaque appel de la méthode getInstance().
Mais cela n’est pas efficace. Je vous laisse lire cette note « The « Double-Checked Locking is Broken » Declaration »
Une note signée par des grandes références de Java dont Joshua Bloch.
La technique du Holder
La dernière solution consisterait à utiliser la technique d’un Holder. Autrement dit, il s’agit d’utiliser une classe interne statique privée, qui se renverra l’instance “INSTANCE“ de type de la classe mère.
La différence majeure entre le Singleton Classique et ce Holder réside surtout dans l’instanciation de la statique.
Dans le premier cas, celui-ci sera initialisé lors du passage de la classe dans le ClassLoader. Avec un Holder, notre variable instance sera initialisée UNIQUEMENT lorsque l’on appellera notre classe. L’instanciation sera faite le plus tard possible.
public class HolderSingleton {
/*Using private static class to get the new INSTANCE of HolderSingleton*/
/*Static is used to get a new instance to the first call to HolderSingleton*/
private static class Holder{
public static HolderSingleton INSTANCE = new HolderSingleton();
}
/*private Constructor*/
private HolderSingleton(){}
public static HolderSingleton getInstance() {
return Holder.INSTANCE;
}
public void sayHello(String s) {
System.out.println("Class "+s+" : " + this);
}
}
Cette solution est la plus complète et permet de gérer la mécanique de la concurrence de manière transparente ! Parmi les éléments déjà présentés, la technique du Holder serait celle à privilégier.
Et l’instanciation par d’autres mécanismes ?
Tous les modèles que nous avons présentés ici possèdent des qualités mais aussi des faiblesses.
Une autre chose à laquelle il faut aussi penser est la sérialisation des données par exemple.
En effet lorsque nous récupérons des objets sérialisés, par défaut Java va instancier une nouvelle fois notre classe.
Pour éviter cela, il faut penser à surcharger la méthode readResolve() de l’interface Serializable en renvoyant l’instance créé.
Nous pouvons aussi utiliser des Frameworks Java très performants tels que Spring (IoC) par exemple. Nous parlerons de Spring dans un billet futur.
public class SerializableStaticSingleton implements Serializable {
private static SerializableStaticSingleton INSTANCE =
new SerializableStaticSingleton();
private SerializableStaticSingleton() { }
public static SerializableStaticSingleton getInstance() {
return INSTANCE;
}
public Object readResolve() throws ObjectStreamException {
return SerializableStaticSingleton.getInstance();
}
public void sayHello(String s) {
System.out.println("Class "+s+": " + this);
}
}
Et ceci n’est qu’un exemple.
Utiliser les énumérateurs !
Beaucoup de choses à penser finalement quand on ne veut créer qu’un simple singleton …
Selon Joshua Bloch dans son livre : « Effective Java 2nd Edition », la meilleure façon de modéliser un Singleton est de passer par un énumérateur ! Extrait ici
public enum MyEnumSingleton implements Serializable{
INSTANCE;
private int x = 0;
/* Method to represent the state of this Singleton*/
public void sayHello(String s) {
System.out.println("Class "+s+": " + this + " Count = : " + x++);
}
}
Exemple :
public class HelloEnumSingleton implements Runnable{
private Integer cpt;
public HelloEnumSingleton(int c) {
cpt = c;
}
public HelloEnumSingleton() {
cpt = 0;
}
public void run() {
MyEnumSingleton s = MyEnumSingleton.INSTANCE;
s.sayHello(cpt.toString());
}
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread[] th = new Thread[10];
for (int i=0;i<10;i++) {
th[i] = (new Thread(new HelloEnumSingleton(i)));
th[i].start();
}
for (Thread t : th)
t.join();
System.out.println("Time taken : " +
(System.currentTimeMillis()-start) +
"ms");
}
}
Output :
Class 0: INSTANCE Count = : 0
Class 8: INSTANCE Count = : 6
Class 5: INSTANCE Count = : 5
Class 4: INSTANCE Count = : 8
Class 9: INSTANCE Count = : 9
Class 6: INSTANCE Count = : 4
Class 7: INSTANCE Count = : 3
Class 2: INSTANCE Count = : 2
Class 1: INSTANCE Count = : 1
Class 3: INSTANCE Count = : 7
Time taken : 1ms
Un « enum » en Java nous permet de toujours garder une seule instance, et gère de manière native la sérialisation et autres mécanismes.
Forme UML pour un énumérateur
Comme je vous l’ai dit au préalable, la forme UML exposée était une approche fonctionnelle indépendante du langage.
Voici ci-dessous une forme plus adaptée.
Conclusion
La modélisation d’un Singleton 100% safe est très difficile avec les évolutions des langages.
Ici plusieurs modélisations ont été présentées avec des vrais problèmes pour certaines solutions mais d’autres peuvent tout à fait être adaptés.
Le plus couramment utilisé dans nos applications sera sans doute notre Singleton classique. Ce qui n’est pas une erreur.
L’ensemble de ces Singleton vous permettront d’avoir les forces et faiblesses de la plus part des types rencontrés en entreprise.