Bilan sur le pattern Singleton

S’il est un pattern sur lequel on ne cesse de revenir depuis son introduction par le célèbre GoF en 1994 dans Design Patterns: Elements of Reusable Object-Oriented Software, c’est bien celui-la. Entre les remarques sur le fait qu’il n’est pas thread safe, les différentes façons de lever ce problème, le fait qu’il utilise une variable statique, (ce que certains considèrent comme une hérésie), on n’en finit plus de faire la liste de ce qu’il faut faire ou ne pas faire.

Je voudrais tenter dans cet article de faire le bilan de toutes ces discussions, éparpillées à droite et à gauche sur le net, en y ajoutant un peu de réflexion personnelle. N’hésitez pas à me faire part de vos commentaires, et de vos retours d’expérience !

Qu’est-ce qu’un singleton ?

Commençons par le commencement. D’après le GoF, il s’agit d’une classe qui ne peut posséder qu’une unique instance. Le pattern qu’ils donnent est le suivant :

public class Singleton {

   private static Singleton instance = new Singleton() ;

   private Singleton() {
   }

   public static Singleton getInstance() {
      return instance ;
   }
}

Le constructeur privé empêche l’instanciation de cette classe par un new. On ne peut accèder à l’unique instance de cette classe qu’en interrogeant la méthode getInstance() déclarée dans la classe elle-même. Cette première version est très classique.

On peut procéder différemment, avec une classe holder, qui possède la référence vers cette unique instance.

public class SingletonHolder {

   private static Singleton instance = new Singleton() ;

   public Singleton getInstance() {
      return instance ;
   }
}

Dans ce deuxième cas, on peut avoir autant d’instances de SingletonHolder que l’on veut, mais il n’y aura jamais qu’une unique instance de la classe Singleton. Notons qu’il faut « cacher » d’une façon ou d’une autre la classe Singleton, puisqu’il est possible de l’instancier d’une autre classe qu’elle-même.

Ce pattern est l’objet de deux reproches principaux :

  • La construction de l’instance de ce singleton se fait au chargement de la classe. De nombreuses personnes préfère que cette instanciation se fasse en mode lazy, c’est-à-dire au moment ou l’application en a besoin. Ce point de vue peut se justifier si cette instanciation est coûteuse, ou si l’on souhaite avoir un contrôle fin de l’ordre de l’instanciation d’un ensemble de singletons pour une application donnée.
  • L’unique instance de cette classe est stockée dans une variable statique. Ce point est relevé par Cédric Beust dans son blog. Son argument principal est que l’utilisation de variables statiques en général stocke des variables dans un contexte lié directement à la JVM. Une variable statique n’est jamais retirée de la mémoire, dans la mesure où elle est attachée à une classe, et qu’une classe est toujours présente une fois chargée (disons que c’est le plus souvent le cas). Ce problème peut être résolu en utilisant un injecteur de dépendances.

Instanciation lazy, concurrence d’accès

En tentant une instanciation dans ce mode, on entre réellement dans les problèmes de ce pattern. Le code le plus souvent rencontré est le suivant.

public class Singleton {

   private static Singleton instance ;

   private Singleton() {
   }

   public static Singleton getInstance() {
      if (instance == null) {
         instance = new Singleton() ;
      }
      return singleton ;
   }
}

Le problème de cette façon de faire est bien connu : en cas de concurrence sur l’accès à ce singleton, on risque fort de se retrouver avec plusieurs instances. Ce qui, reconnaissons-le est un peu problématique pour un singleton. Je dis « un peu », car il y a des cas dans lesquels avoir quelques instances n’est pas vraiment un problème. Nous discuterons de ce point dans la suite.

Les patterns qui permettent de gérer cette concurrence d’accès passent par la synchronisation de la construction. Le premier pattern est simple, il consiste à synchroniser purement et simplement la méthode d’accès.

public class Singleton {
   private static Singleton instance ;

   private Singleton() {
   }

   public static synchronized Singleton getInstance() {
      if (instance == null) {
         instance = new Singleton() ;
      }
      return singleton ;
   }
}

Cette façon de faire est un peu brutale, et un peu fragile.

Fragile parce que ce n’est jamais une bonne idée de synchroniser un accès en utilisant un objet (ici la classe Singleton directement) que tout le monde peut lire. Disons plus précisément que ne pas le faire permet de mieux se protéger contre les deadlock.

Brutale parce que l’accès au singleton est systématiquement synchronisé, que ce soit pour une lecture ou pour l’unique écriture. Or, ce que l’on veut faire, c’est synchroniser uniquement l’unique écriture. Suivant cette idée, certains ont proposé un pattern plus sophistiqué, le double checked locking.

Double checked locking

L’idée de ce pattern est de permettre une lecture non synchronisée du singleton, réputée plus performante qu’une lecture synchronisée, tout en contrôlant la concurrence d’accès à la création.

public class Singleton {

   private static Singleton instance ;

   private Singleton() {
   }

   public static Singleton getInstance() {
      if (instance == null) {
         synchronized(Singleton.class) {
            if (instance == null) {
               instance = new Singleton() ;
            }
         }
      }
      return singleton ;
   }
}

Pourquoi cette double vérification ? Tout simplement parce que si la concurrence d’accès est forte, plusieurs threads pourraient entrer dans le premier bloc if (ligne 9), et ainsi créer plusieurs instances de notre singleton, les uns à la suite des autres.

Ce pattern peut paraître séduisant, et il peut même fonctionner dans le contexte de machines monoprocesseurs et monocœurs. Malheureusement, il comporte un bug, assez subtil.

Pour le comprendre, il faut faire un petit détour par ce que l’on appelle le Java Memory Model. La lecture du champ instance (ligne 9) n’est pas synchronisée. Le cas pathologique est celui dans lequel un premier thread T1 a déjà instancié le champ instance, mais n’est pas encore sorti de son bloc synchronisé d’écriture (ce qu’il fait ligne 14). Un deuxième thread T2 tente de lire le champ instance, afin de savoir s’il est nul ou pas. Comme cette lecture n’est pas synchronisée, la valeur du champ instance n’est pas définie. Il se peut que ce soit la valeur fixée par T1, null dans le cas d’un objet, ou une valeur intermédiaire entre les deux (s’il s’agit d’un long ou d’un double, cette valeur peut avoir ses 32 bits de poids faible à la bonne valeur, et les 32 autres nuls !).

Depuis Java 5, on peut lever ce problème en déclarant le champ instance volatile. Mais cela ne résout le problème que partiellement. S’il s’agit d’une référence sur un objet, alors il faut que les champs de cet objet soient également déclarés volatile. Sinon un thread qui n’aurait pas initialisé cet objet pourrait lire des valeurs erronées, typiquement nulles.

Comme on le voit, ce double checked locking ne résout pas vraiment le problème, en tout cas pas dans le cas général.

Ce qui me gène le plus dans cette solution, c’est que l’on doit déclarer volatile tout un tas de champs qui n’ont qu’un rapport assez lointain avec le singleton. Une fois le code écrit et testé (mais le sera-t-il dans tous les cas de concurrence d’accès ? Rien n’est moins sûr…), des refactorings sauvages retireront à coup sûr ces déclarations volatile. Les bugs générés seront très délicats à identifier, et risquent de ne surgir qu’en fonctionnement très chargé et très concurrent, c’est-à-dire en production.

Instanciation par classe interne statique

L’instanciation lazy peut aussi être réalisée par l’introduction d’une classe interne statique.

public class Singleton {

   private Singleton() {
   }

   private static class Holder {
      public static Singleton instance = new Singleton() ;
   }

   public Singleton getInstance() {
      return Hodler.instance ;
   }
}

Cette classe n’est chargée par la machine Java qu’au dernier moment, ce qui garantit l’instanciation lors de l’appel à la méthode getInstance(), et pas avant.

Ce pattern ne pose pas de problème de concurrence d’accès. Ce point est géré par la JVM elle-même.

Instanciation à l’aide d’une énumération

Dans la deuxième édition de Effective Java, Joshua Bloch (auteur en qui on peut avoir confiance !) écrit que la meilleure façon de créer un singleton est d’utiliser une énumération. Cela a au moins le mérite de simplifier grandement le code !

public enum Singleton {

   INSTANCE
}

On peut écrire des constructeurs privés pour les énumérations, et leur déclarer des champs, ce qui donne toute liberté pour créer des objets arbitrairement complexes. J’ai écrit une page sur les énumérations, et notamment l’utilisation des constructeurs privés, qui se trouve ici.

Utilisation du ServiceLoader

Une des nouveautés de Java 6 est l’introduction d’un mécanisme de chargement de service. En toute rigueur, ce mécanisme était déjà présent depuis Java 3, mais n’a été versé à l’API publique qu’en version 6. Bon, depuis le temps qu’il est « nouveau », Java 6, il va bien finir par devenir has been lui aussi… Il paraît même qu’il serait grand temps !

En deux mots, un service est une interface. Plusieurs classes peuvent implémenter cette interface. Le ServiceLoader est simple à mettre en œuvre. Il suffit de créer un fichier dans le répertoire META-INF/services accessible du classloader qui connaît l’implémentation de ce service. Ce fichier doit porter le nom complet de l’interface dont on déclare l’implémentation, et contenir les noms complets des classes qui implémentent cette interface. Ce mécanisme ne permet pas de créer des singletons en lui-même. En revanche, il peut être utilisé pour instancier des singletons à partir de classes holder.

Instanciation par introspection

Empêcher strictement l’instanciation directe d’une classe n’est pas une chose si facile. Rendre le constructeur vide de la classe privé est une première avancée, dans la mesure où cela empêche d’écrire des extensions de cette classe (on ne pourrait pas les construire).

Mais cela n’empêche pas d’instancier la classe par introspection. Il suffit pour cela de rendre le constructeur accessible (mise à true de la propriété accessible de l’objet Constructor associé à ce constructeur vide). La seule façon d’empêcher cette possibilité est d’activer le SecurityManager, et de ne pas donner la permission suppressAccessChecks. Activer ce SecutityManager n’est pas toujours possible en pratique, ou simple.

Restent deux idées. La première pourrait consister à tester si l’objet Constructor qui sert à instancier cette classe par introspection a sa propriété accessible à true ou pas. Mais l’API est ainsi faite que si deux méthodes demandent une instance du constructeur vide privé, deux instances différentes seront fournies. Tester la valeur de accessible dans l’une ne donne donc aucune information sur l’autre.

La seconde consiste à examiner la pile d’appel : suis-je bien appelé par la méthode getInstance() ? Cela donne le code suivant.

public class Singleton {

	private static Singleton instance ;

	private Singleton() throws Exception {

		StackTraceElement[] elements = Thread.currentThread().getStackTrace() ;

		if (!"getInstance".equals(elements[2].getMethodName())) {
			throw new Exception("Construction par méthode getInstance obligatoire !") ;
		}
	}

	public static Singleton getInstance() {

		if (instance == null) {
			try  {
				instance = new Singleton() ;
			} catch (Exception e) {
				// Gérer l'exception
			}
		}

		return instance ;
	}
}

Le lecteur (précieux) averti aura remarqué l’utilisation d’un magnifique pattern™ : le handle the exception later pattern. J’ai toujours trouvé très curieux que ce pattern, si souvent rencontré dans la pratique, ait été oublié par le GoF…

Il aura aussi remarqué l’extraordinaire robustesse de ce pattern, parfaitement résistant au changement de nom de la méthode getInstance(). Cela dit, il bloque la construction par introspection, c’est ce que nous voulions.

Serialization

Si la classe Singleton est Serializable, alors il devient possible d’écrire son instance dans un tableau d’octets (en mémoire donc), et de le reconstituer en mémoire plusieurs fois, créant ainsi plusieurs instances de Singleton. Si la classe Singleton a été déclarée Serializable, c’est que son instance doit pouvoir être écrite ou relue sur un flux, qu’il soit d’un type ou d’un autre. Empêcher cette relecture comme on le lit parfois, en utilisant une technique ou une autre (il y en a au moins deux), est donc en pratique, très probablement une mauvaise idée™.

Empêcher la duplication d’un singleton par sérialization, si cette classe a été déclarée Serializable n’est donc pas possible.

Empêcher les singletons multiples

Empêcher strictement la multiplicité d’un singleton n’est pas possible dans de nombreux cas, comme nous venons de le voir. Cela pose-t-il un problème ?

Je pense que cela dépend du singleton. Dans de nombreux cas pratiques, un singleton est un objet sans état. Si l’on veut éviter de le multiplier c’est juste que son instanciation est coûteuse. En créer plusieurs instances se traduira donc par un surcoût en ressources, et non pas par un dysfonctionnement du système.

Ce n’est peut-être pas très satisfaisant pour l’esprit, mais c’est un moindre mal. Ce surcoût sera-t-il rédhibitoire ? Ce n’est même pas certain, car dans de nombreux cas il ne sera payé qu’au lancement de l’application.

Empêcher cette multiplicité repose aussi sur le respect des patterns de création par les développeurs. Dresser des barrières coûteuses pour prévenir cette multiplicité peut signifier que la documentation écrite sur le code existant, ses API et ses patterns d’utilisation n’est pas prise en compte correctement, et que la confiance que l’on a dans ses équipes de développement est limitée. Dans ce cas, il sera probablement plus malin de s’attaquer à ce problème.

Singleton, classloader et JVM

Quoique l’on fasse, un singleton est effectivement unique au sein d’un classloader donné, celui qui porte la classe dont ce singleton est une instance. Cette limitation est une évidence, et comme toutes les évidences, ça va mieux en le disant. Idem pour une application répartie entre plusieurs JVM, une même instance de classe ne peut être partagée entre plusieurs JVM.

Références

Wikipedia a une page minimaliste sur le sujet : http://en.wikipedia.org/wiki/Singleton_pattern

William Pugh sur le Java Memory Modelhttp://www.cs.umd.edu/~pugh/java/memoryModel/

Doug Lea sur la volatilité : http://gee.cs.oswego.edu/dl/cpj/jmm.html (cette page a été écrite en 2000, et n’est donc pas à jour sur Java 5)

Brian Goëtz  sur le double-checked lockinghttp://www.javaworld.com/jw-02-2001/jw-0209-double.html (cet article a été écrit en 2001 et n’est plus à jour sur la volatilité).

3 réflexions au sujet de « Bilan sur le pattern Singleton »

  1. C’est bien beau ces exemples, mais pour conclure, de quelle manière faudrait-t-il procéder pour créer sa classe Singleton sans qu’il y ait de problème ? (compatible avec les langages autre que Java).
    Merci 🙂

    1. En Java le meilleur pattern est de faire une énumération. Cet article porte sur Java, donc pour les autres langages, voir avec leurs experts respectifs !

Les commentaires sont fermés.