Le patron de conception Singleton en Java
Implémentez le patron Singleton en Java avec les approches eager, lazy et basée sur les enums, de manière thread-safe.
Le patron singleton restreint une classe à une seule instance et fournit un point d'accès global à celle-ci. Une façade de journalisation, un registre de configuration, un cache en mémoire — voilà le genre de choses qui s'y prêtent bien. En Java, ce patron se décline sous plusieurs formes standard, chacune avec ses propres compromis en matière de thread-safety et de chargement différé. Il existe également une approche qui évite discrètement la plupart des problèmes.
Un mot de prudence pour commencer. Le « singleton » est notoirement facile à surutiliser — chaque singleton est, en pratique, une variable globale, et les variables globales rendent le code plus difficile à tester et à comprendre. La plupart des applications Java modernes préfèrent l'injection de dépendances : le framework câble une instance unique et la transmet aux composants qui en ont besoin, sans qu'aucun d'eux ait à appeler Foo.getInstance(). Recourez à un singleton explicite quand l'injection de dépendances n'est pas disponible ou s'avère vraiment excessive.
Ce que tout singleton requiert
Toute implémentation de singleton partage trois éléments :
- Un constructeur privé, pour qu'aucune classe externe ne puisse appeler
new. - Un champ statique privé contenant l'instance unique.
- Un accesseur statique public qui la retourne.
Les variantes portent essentiellement sur quand l'instance est créée et comment l'accès reste thread-safe.
Initialisation hâtive (eager initialization)
La forme la plus simple crée l'instance au chargement de la classe :
public final class Eager {
private static final Eager INSTANCE = new Eager();
private Eager() {}
public static Eager getInstance() { return INSTANCE; }
}L'initialisation de classe dans la JVM est garantie d'être thread-safe et de ne s'exécuter qu'une seule fois, donc INSTANCE est défini en toute sécurité sans verrous. Utilisez cette approche quand :
- La construction est peu coûteuse, ou vous savez que vous aurez toujours besoin de l'instance.
- Le coût au moment du chargement de la classe ne vous pose pas de problème.
Initialisation différée avec double-checked locking
Si la construction est coûteuse et que vous pourriez ne jamais avoir besoin de l'instance, vous pouvez la différer. La version naïve différée n'est pas thread-safe ; la version correcte utilise le double-checked locking avec volatile :
public final class Lazy {
private static volatile Lazy instance;
private Lazy() {}
public static Lazy getInstance() {
Lazy local = instance; // local read avoids re-reading the volatile field
if (local == null) {
synchronized (Lazy.class) {
local = instance;
if (local == null) {
local = new Lazy();
instance = local;
}
}
}
return local;
}
}volatile est essentiel — sans lui, un autre thread pourrait voir le champ défini à une référence non-null dont le constructeur n'a pas encore terminé. Verbeux, mais correct.
Le patron holder à initialisation à la demande
La forme différée la plus élégante utilise une classe interne privée. La JVM ne charge le holder que lorsque quelqu'un appelle getInstance() pour la première fois, ce qui diffère le travail — et les garanties d'initialisation de classe de la JVM gèrent la thread-safety :
public final class Holder {
private Holder() {}
private static class H {
private static final Holder INSTANCE = new Holder();
}
public static Holder getInstance() { return H.INSTANCE; }
}Pas de synchronized, pas de volatile, pas de double-checked locking — et pourtant différé. C'est la forme différée à utiliser dans la plupart des codes.
Le singleton enum
Le singleton correct le plus court en Java est un enum avec une seule constante :
public enum Config {
INSTANCE;
public String get(String key) { /* ... */ }
}Config.INSTANCE est le singleton. La recommandation de Joshua Bloch (Effective Java, Item 3) est qu'il s'agit de la meilleure implémentation de singleton, car la JVM garantit :
- Exactement une instance. Les enums sont construits exactement une fois par JVM.
- Construction thread-safe. Mêmes garanties d'initialisation de classe que le patron holder.
- Protection contre la réflexion. La réflexion ne peut pas invoquer le constructeur d'un enum ; les singletons ordinaires peuvent être contournés avec
Constructor.setAccessible(true). - Protection contre la sérialisation. La désérialisation d'un singleton normal peut produire silencieusement une seconde instance si vous ne gérez pas
readResolve. Les enums sont immunisés.
La seule chose qu'il ne peut pas faire est d'étendre une autre classe — les enums étendent implicitement java.lang.Enum. Il peut toutefois implémenter des interfaces.
Ce qui brise les singletons naïfs
Faites attention à ces points — c'est pourquoi « utiliser simplement un champ statique » ne suffit pas toujours :
- Plusieurs chargeurs de classes. Un singleton est un-par-classloader, pas un-par-JVM. Dans des conteneurs qui isolent les applications avec leurs propres loaders, la même classe peut avoir plusieurs « l'unique » instances.
- Réflexion.
setAccessible(true)combiné àConstructor.newInstance()peut créer une deuxième instance de tout singleton non-enum. Protégez le constructeur avecif (INSTANCE != null) throw ...si c'est une vraie préoccupation. - Sérialisation. Un singleton
Serializablea besoin deprivate Object readResolve() { return INSTANCE; }pour éviter de produire une deuxième copie à chaque désérialisation. - Tests. Les singletons sont notoirement difficiles à substituer ou réinitialiser. Préférez l'injection de dépendances dans le code que vous comptez tester unitairement.
Un exemple concret
La suite
Cela conclut la Partie 6 et l'ensemble du tour d'horizon de Java orienté objet — des classes, de l'héritage et du polymorphisme en passant par les interfaces, les enums, les records, les hiérarchies scellées et les méthodes que chaque objet hérite. La prochaine partie prend du recul sur la façon dont le code Java est organisé : les espaces de noms, la structure de système de fichiers qui les reflète, et le mécanisme import qui importe des types d'autres endroits vers le vôtre. Continuez avec Les packages Java.