Comment naviguer dans le modèle Singleton trompeusement simple

Le modèle Singleton est d'une simplicité trompeuse, même et surtout pour les développeurs Java. Dans cet article JavaWorld classique , David Geary montre comment les développeurs Java implémentent des singletons, avec des exemples de code pour le multithreading, les chargeurs de classes et la sérialisation à l'aide du modèle Singleton. Il conclut par un regard sur l'implémentation des registres singleton afin de spécifier des singletons au moment de l'exécution.

Parfois, il est approprié d'avoir exactement une instance d'une classe: les gestionnaires de fenêtres, les spouleurs d'impression et les systèmes de fichiers sont des exemples prototypiques. En règle générale, ces types d'objets, appelés singletons, sont accessibles par des objets disparates dans un système logiciel et nécessitent donc un point d'accès global. Bien sûr, juste au moment où vous êtes certain de ne jamais avoir besoin de plus d'une instance, il y a fort à parier que vous changerez d'avis.

Le modèle de conception Singleton répond à toutes ces préoccupations. Avec le modèle de conception Singleton, vous pouvez:

  • Assurez-vous qu'une seule instance d'une classe est créée
  • Fournir un point d'accès global à l'objet
  • Autoriser plusieurs instances à l'avenir sans affecter les clients d'une classe singleton

Bien que le modèle de conception Singleton - comme le montre la figure ci-dessous - soit l'un des modèles de conception les plus simples, il présente un certain nombre de pièges pour le développeur Java imprudent. Cet article décrit le modèle de conception Singleton et aborde ces pièges.

En savoir plus sur les modèles de conception Java

Vous pouvez lire toutes les colonnes des modèles de conception Java de David Geary ou consulter une liste des articles les plus récents de JavaWorld sur les modèles de conception Java. Voir « Modèles de conception, vue d'ensemble » pour une discussion sur les avantages et les inconvénients de l'utilisation des modèles Gang of Four. Vouloir plus? Recevez la newsletter Enterprise Java dans votre boîte de réception.

Le motif Singleton

Dans Design Patterns: Elements of Reusable Object-Oriented Software , le Gang of Four décrit le modèle Singleton comme ceci:

Assurez-vous qu'une classe n'a qu'une seule instance et fournissez-lui un point d'accès global.

La figure ci-dessous illustre le diagramme de classes du modèle de conception Singleton.

Comme vous pouvez le voir, il n'y a pas grand-chose dans le modèle de conception Singleton. Les singletons conservent une référence statique à la seule instance de singleton et renvoient une référence à cette instance à partir d'une instance()méthode statique .

L'exemple 1 montre une implémentation de modèle de conception Singleton classique:

Exemple 1. Le singleton classique

public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }

Le singleton implémenté dans l'exemple 1 est facile à comprendre. La ClassicSingletonclasse conserve une référence statique à l'instance de singleton unique et renvoie cette référence à partir de la getInstance()méthode statique .

Il y a plusieurs points intéressants concernant la ClassicSingletonclasse. Premièrement, ClassicSingletonutilise une technique connue sous le nom d' instanciation paresseuse pour créer le singleton; par conséquent, l'instance singleton n'est pas créée tant que la getInstance()méthode n'est pas appelée pour la première fois. Cette technique garantit que les instances singleton ne sont créées qu'en cas de besoin.

Deuxièmement, notez que ClassicSingletonimplémente un constructeur protégé afin que les clients ne puissent pas instancier des ClassicSingletoninstances; cependant, vous serez peut-être surpris de découvrir que le code suivant est parfaitement légal:

public class SingletonInstantiator { public SingletonInstantiator() { ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance =new ClassicSingleton(); ... } }

Comment la classe du fragment de code précédent - qui ne s'étend pas - peut-elle ClassicSingletoncréer une ClassicSingletoninstance si le ClassicSingletonconstructeur est protégé? La réponse est que les constructeurs protégés peuvent être appelés par des sous-classes et par d'autres classes du même package . Comme ClassicSingletonet SingletonInstantiatorsont dans le même package (le package par défaut), les SingletonInstantiator()méthodes peuvent créer des ClassicSingletoninstances. Ce dilemme a deux solutions: Vous pouvez rendre le ClassicSingletonconstructeur privé de sorte que seules les ClassicSingleton()méthodes l'appellent; cependant, cela signifie ClassicSingletonne peut pas être sous-classé. Parfois, c'est une solution souhaitable; si c'est le cas, c'est une bonne idée de déclarer votre classe singletonfinal, ce qui rend cette intention explicite et permet au compilateur d'appliquer des optimisations de performances. L'autre solution consiste à placer votre classe singleton dans un package explicite, de sorte que les classes d'autres packages (y compris le package par défaut) ne peuvent pas instancier des instances singleton.

Un troisième point intéressant à propos de ClassicSingleton: il est possible d'avoir plusieurs instances de singleton si des classes chargées par différents chargeurs de classe accèdent à un singleton. Ce scénario n'est pas si exagéré; par exemple, certains conteneurs de servlet utilisent des chargeurs de classe distincts pour chaque servlet, donc si deux servlets accèdent à un singleton, ils auront chacun leur propre instance.

Quatrièmement, si ClassicSingletonimplémente l' java.io.Serializableinterface, les instances de la classe peuvent être sérialisées et désérialisées. Toutefois, si vous sérialisez un objet singleton et que vous désérialisez par la suite cet objet plusieurs fois, vous aurez plusieurs instances de singleton.

Enfin, et peut-être le plus important, la ClassicSingletonclasse de l' exemple 1 n'est pas thread-safe. Si deux threads - nous les appellerons Thread 1 et Thread 2 - appellent ClassicSingleton.getInstance()en même temps, deux ClassicSingletoninstances peuvent être créées si Thread 1 est préempté juste après son entrée dans le ifbloc et le contrôle est ensuite donné à Thread 2.

Comme vous pouvez le voir dans la discussion précédente, bien que le modèle Singleton soit l'un des modèles de conception les plus simples, son implémentation en Java est tout sauf simple. Le reste de cet article traite des considérations spécifiques à Java pour le modèle Singleton, mais commençons par faire un petit détour pour voir comment vous pouvez tester vos classes singleton.

Singletons de test

Dans le reste de cet article, j'utilise JUnit de concert avec log4j pour tester les classes singleton. Si vous n'êtes pas familier avec JUnit ou log4j, voir Ressources.

L'exemple 2 répertorie un cas de test JUnit qui teste le singleton de l'exemple 1:

Exemple 2. Un cas de test singleton

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); Assert.assertEquals(true, sone == stwo); } }

Le cas de test de l'exemple 2 appelle ClassicSingleton.getInstance()deux fois et stocke les références renvoyées dans des variables membres. La testUnique()méthode vérifie que les références sont identiques. L'exemple 3 montre la sortie du scénario de test:

Exemple 3. Sortie de scénario de test

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java] .INFO main: getting singleton... [java] INFO main: created singleton: [email protected] [java] INFO main: ...got singleton: [email protected] [java] INFO main: getting singleton... [java] INFO main: ...got singleton: [email protected] [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

Comme l'illustre la liste précédente, le test simple de l'exemple 2 passe avec brio - les deux références de singleton obtenues avec ClassicSingleton.getInstance()sont en effet identiques; cependant, ces références ont été obtenues dans un seul thread. La section suivante teste notre classe singleton avec plusieurs threads.

Considérations relatives au multithreading

La ClassicSingleton.getInstance()méthode de l' exemple 1 n'est pas thread-safe en raison du code suivant:

1: if(instance == null) { 2: instance = new Singleton(); 3: }

If a thread is preempted at Line 2 before the assignment is made, the instance member variable will still be null, and another thread can subsequently enter the if block. In that case, two distinct singleton instances will be created. Unfortunately, that scenario rarely occurs and is therefore difficult to produce during testing. To illustrate this thread Russian roulette, I've forced the issue by reimplementing Example 1's class. Example 4 shows the revised singleton class:

Example 4. Stack the deck

import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread.Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }

Example 4's singleton resembles Example 1's class, except the singleton in the preceding listing stacks the deck to force a multithreading error. The first time the getInstance() method is called, the thread that invoked the method sleeps for 50 milliseconds, which gives another thread time to call getInstance() and create a new singleton instance. When the sleeping thread awakes, it also creates a new singleton instance, and we have two singleton instances. Although Example 4's class is contrived, it stimulates the real-world situation where the first thread that calls getInstance() gets preempted.

Example 5 tests Example 4's singleton:

Example 5. A test that fails

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call Singleton.getInstance(). Thread threadOne = new Thread(new SingletonTestRunnable()), threadTwo = new Thread(new SingletonTestRunnable()); threadOne.start();threadTwo.start(); threadOne.join(); threadTwo.join(); } private static class SingletonTestRunnable implements Runnable { public void run() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }

Example 5's test case creates two threads, starts each one, and waits for them to finish. The test case maintains a static reference to a singleton instance, and each thread calls Singleton.getInstance(). If the static member variable has not been set, the first thread sets it to the singleton obtained with the call to getInstance(), and the static member variable is compared to the local variable for equality.

Voici ce qui se passe lorsque le scénario de test s'exécute: Le premier thread appelle getInstance(), entre dans le ifbloc et se met en veille. Par la suite, le deuxième thread appelle getInstance()et crée également une instance de singleton. Le deuxième thread définit ensuite la variable membre statique sur l'instance qu'il a créée. Le deuxième thread vérifie l'égalité de la variable membre statique et de la copie locale, et le test réussit. Lorsque le premier thread se réveille, il crée également une instance de singleton, mais ce thread ne définit pas la variable membre statique (car le deuxième thread l'a déjà définie), donc la variable statique et la variable locale ne sont pas synchronisées, et le test car l'égalité échoue. L'exemple 6 répertorie la sortie du cas de test de l'exemple 5: