Cómo navegar por el patrón Singleton engañosamente simple

El patrón Singleton es engañosamente simple, uniforme y especialmente para los desarrolladores de Java. En este artículo clásico de JavaWorld , David Geary demuestra cómo los desarrolladores de Java implementan singletons, con ejemplos de código para multiproceso, cargadores de clases y serialización utilizando el patrón Singleton. Concluye con un vistazo a la implementación de registros singleton para especificar singletons en tiempo de ejecución.

A veces es apropiado tener exactamente una instancia de una clase: los administradores de ventanas, los spoolers de impresión y los sistemas de archivos son ejemplos prototípicos. Por lo general, a esos tipos de objetos, conocidos como singletons, se accede mediante objetos dispares en un sistema de software y, por lo tanto, requieren un punto de acceso global. Por supuesto, justo cuando esté seguro de que nunca necesitará más de una instancia, es una buena apuesta que cambiará de opinión.

El patrón de diseño de Singleton aborda todas estas preocupaciones. Con el patrón de diseño Singleton puedes:

  • Asegúrese de que solo se cree una instancia de una clase
  • Proporcionar un punto de acceso global al objeto.
  • Permita múltiples instancias en el futuro sin afectar a los clientes de una clase única

Aunque el patrón de diseño de Singleton, como lo demuestra la figura siguiente, es uno de los patrones de diseño más simples, presenta una serie de dificultades para el desarrollador de Java desprevenido. Este artículo analiza el patrón de diseño de Singleton y aborda esas dificultades.

Más sobre los patrones de diseño de Java

Puede leer todas las columnas de patrones de diseño de Java de David Geary o ver una lista de los artículos más recientes de JavaWorld sobre patrones de diseño de Java. Consulte " Patrones de diseño, el panorama general " para ver una discusión sobre los pros y los contras de usar los patrones Gang of Four. ¿Quieren más? Reciba el boletín de Enterprise Java en su bandeja de entrada.

El patrón Singleton

En Patrones de diseño: elementos de software orientado a objetos reutilizable , el grupo de los cuatro describe el patrón Singleton de la siguiente manera:

Asegúrese de que una clase tenga solo una instancia y proporcione un punto global de acceso a ella.

La siguiente figura ilustra el diagrama de clases del patrón de diseño Singleton.

Como puede ver, no hay mucho en el patrón de diseño de Singleton. Los singleton mantienen una referencia estática a la única instancia de singleton y devuelven una referencia a esa instancia desde un instance()método estático .

El ejemplo 1 muestra una implementación clásica de patrón de diseño Singleton:

Ejemplo 1. El singleton clásico

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; } }

El singleton implementado en el Ejemplo 1 es fácil de entender. La ClassicSingletonclase mantiene una referencia estática a la única instancia de singleton y devuelve esa referencia del getInstance()método estático .

Hay varios puntos interesantes sobre la ClassicSingletonclase. Primero, ClassicSingletonemplea una técnica conocida como instanciación perezosa para crear el singleton; como resultado, la instancia de singleton no se crea hasta getInstance()que se llama al método por primera vez. Esta técnica garantiza que las instancias de singleton se creen solo cuando sea necesario.

En segundo lugar, observe que ClassicSingletonimplementa un constructor protegido para que los clientes no puedan crear ClassicSingletoninstancias; sin embargo, es posible que se sorprenda al descubrir que el siguiente código es perfectamente legal:

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

¿Cómo puede la clase en el fragmento de código anterior, que no se extiende, ClassicSingletoncrear una ClassicSingletoninstancia si el ClassicSingletonconstructor está protegido? La respuesta es que los constructores protegidos pueden ser llamados por subclases y por otras clases en el mismo paquete . Como ClassicSingletony SingletonInstantiatorestán en el mismo paquete (el paquete predeterminado), los SingletonInstantiator()métodos pueden crear ClassicSingletoninstancias. Este dilema tiene dos soluciones: puede hacer que el ClassicSingletonconstructor sea privado para que solo los ClassicSingleton()métodos lo llamen; sin embargo, esos medios ClassicSingletonno se pueden subclasificar. A veces, esa es una solución deseable; si es así, es una buena idea declarar su clase singletonfinal, que hace esa intención explícita y permite al compilador aplicar optimizaciones de rendimiento. La otra solución es poner su clase singleton en un paquete explícito, por lo que las clases en otros paquetes (incluido el paquete predeterminado) no pueden crear instancias de singleton.

Un tercer punto interesante sobre ClassicSingleton: es posible tener múltiples instancias singleton si las clases cargadas por diferentes cargadores de clases acceden a un singleton. Ese escenario no es tan descabellado; por ejemplo, algunos contenedores de servlets usan cargadores de clases distintos para cada servlet, por lo que si dos servlets acceden a un singleton, cada uno tendrá su propia instancia.

Cuarto, si ClassicSingletonimplementa la java.io.Serializableinterfaz, las instancias de la clase se pueden serializar y deserializar. Sin embargo, si serializa un objeto singleton y posteriormente deserializa ese objeto más de una vez, tendrá varias instancias singleton.

Finalmente, y quizás lo más importante, la ClassicSingletonclase del Ejemplo 1 no es segura para subprocesos. Si dos subprocesos (los llamaremos Subproceso 1 y Subproceso 2) llaman ClassicSingleton.getInstance()al mismo tiempo, ClassicSingletonse pueden crear dos instancias si el Subproceso 1 se reemplaza justo después de que ingresa al ifbloque y posteriormente se le da el control al Subproceso 2.

Como puede ver en la discusión anterior, aunque el patrón Singleton es uno de los patrones de diseño más simples, implementarlo en Java es cualquier cosa menos simple. El resto de este artículo aborda las consideraciones específicas de Java para el patrón Singleton, pero primero tomemos un pequeño desvío para ver cómo puede probar sus clases singleton.

Prueba singletons

En el resto de este artículo, utilizo JUnit junto con log4j para probar clases de singleton. Si no está familiarizado con JUnit o log4j, consulte Recursos.

El Ejemplo 2 enumera un caso de prueba JUnit que prueba el singleton del Ejemplo 1:

Ejemplo 2. Un caso de prueba 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); } }

El caso de prueba del ejemplo 2 invoca ClassicSingleton.getInstance()dos veces y almacena las referencias devueltas en variables miembro. El testUnique()método comprueba que las referencias sean idénticas. El ejemplo 3 muestra la salida del caso de prueba:

Ejemplo 3. Salida del caso de prueba

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)

Como ilustra el listado anterior, la prueba simple del Ejemplo 2 pasa con gran éxito: las dos referencias singleton obtenidas con ClassicSingleton.getInstance()son de hecho idénticas; sin embargo, esas referencias se obtuvieron en un solo hilo. La siguiente sección pone a prueba nuestra clase singleton con varios subprocesos.

Consideraciones de subprocesos múltiples

El ClassicSingleton.getInstance()método del ejemplo 1 no es seguro para subprocesos debido al siguiente código:

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.

Esto es lo que sucede cuando se ejecuta el caso de prueba: el primer hilo llama getInstance(), entra en el ifbloque y duerme. Posteriormente, el segundo hilo también llama getInstance()y crea una instancia de singleton. Luego, el segundo subproceso establece la variable miembro estática en la instancia que creó. El segundo subproceso verifica la igualdad de la variable miembro estática y la copia local, y la prueba pasa. Cuando el primer subproceso se despierta, también crea una instancia de singleton, pero ese subproceso no establece la variable miembro estática (porque el segundo subproceso ya lo ha configurado), por lo que la variable estática y la variable local no están sincronizadas, y la prueba porque la igualdad falla. El ejemplo 6 enumera la salida del caso de prueba del ejemplo 5: