Singletons en Java

1. Introducción

En este artículo rápido, discutiremos las dos formas más populares de implementar Singletons en Java simple.

2. Singleton basado en clases

El enfoque más popular es implementar un Singleton creando una clase regular y asegurándose de que tenga:

  • Un constructor privado
  • Un campo estático que contiene su única instancia
  • Un método de fábrica estático para obtener la instancia

También agregaremos una propiedad de información, solo para uso posterior. Entonces, nuestra implementación se verá así:

public final class ClassSingleton { private static ClassSingleton INSTANCE; private String info = "Initial info class"; private ClassSingleton() { } public static ClassSingleton getInstance() { if(INSTANCE == null) { INSTANCE = new ClassSingleton(); } return INSTANCE; } // getters and setters }

Si bien este es un enfoque común, es importante tener en cuenta que puede ser problemático en escenarios de subprocesos múltiples , que es la razón principal para usar Singletons.

En pocas palabras, puede resultar en más de una instancia, rompiendo el principio central del patrón. Aunque existen soluciones de bloqueo para este problema, nuestro próximo enfoque resuelve estos problemas a nivel de raíz.

3. Enum Singleton

En el futuro, no analicemos otro enfoque interesante, que es usar enumeraciones:

public enum EnumSingleton { INSTANCE("Initial class info"); private String info; private EnumSingleton(String info) { this.info = info; } public EnumSingleton getInstance() { return INSTANCE; } // getters and setters }

Este enfoque tiene serialización y seguridad de subprocesos garantizados por la propia implementación de enum, que asegura internamente que solo la instancia única está disponible, corrigiendo los problemas señalados en la implementación basada en clases.

4. Uso

Para usar nuestro ClassSingleton , simplemente necesitamos obtener la instancia estáticamente:

ClassSingleton classSingleton1 = ClassSingleton.getInstance(); System.out.println(classSingleton1.getInfo()); //Initial class info ClassSingleton classSingleton2 = ClassSingleton.getInstance(); classSingleton2.setInfo("New class info"); System.out.println(classSingleton1.getInfo()); //New class info System.out.println(classSingleton2.getInfo()); //New class info

En cuanto al EnumSingleton , podemos usarlo como cualquier otro Java Enum:

EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE.getInstance(); System.out.println(enumSingleton1.getInfo()); //Initial enum info EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE.getInstance(); enumSingleton2.setInfo("New enum info"); System.out.println(enumSingleton1.getInfo()); // New enum info System.out.println(enumSingleton2.getInfo()); // New enum info

5. Errores comunes

Singleton es un patrón de diseño engañosamente simple, y hay pocos errores comunes que un programador puede cometer al crear un singleton.

Distinguimos dos tipos de problemas con singleton:

  • existencial (¿necesitamos un singleton?)
  • implementacional (¿lo implementamos correctamente?)

5.1. Problemas existenciales

Conceptualmente, un singleton es una especie de variable global. En general, sabemos que se deben evitar las variables globales, especialmente si sus estados son mutables.

No estamos diciendo que nunca debamos usar singletons. Sin embargo, estamos diciendo que podría haber formas más eficientes de organizar nuestro código.

Si la implementación de un método depende de un objeto singleton, ¿por qué no pasarlo como parámetro? En este caso, mostramos explícitamente de qué depende el método. Como consecuencia, podemos burlarnos fácilmente de estas dependencias (si es necesario) al realizar las pruebas.

Por ejemplo, los singleton se utilizan a menudo para abarcar los datos de configuración de la aplicación (es decir, la conexión al repositorio). Si se utilizan como objetos globales, resulta difícil elegir la configuración para el entorno de prueba.

Por lo tanto, cuando ejecutamos las pruebas, la base de datos de producción se estropea con los datos de prueba, lo que es poco aceptable.

Si necesitamos un singleton, podríamos considerar la posibilidad de delegar su instanciación a otra clase, una especie de fábrica, que debería encargarse de asegurar que solo haya una instancia del singleton en juego.

5.2. Problemas de implementación

Aunque los singleton parecen bastante simples, sus implementaciones pueden sufrir varios problemas. Todos dan como resultado el hecho de que podríamos terminar teniendo más de una instancia de la clase.

Sincronización

La implementación con un constructor privado que presentamos anteriormente no es segura para subprocesos: funciona bien en un entorno de un solo subproceso, pero en uno de múltiples subprocesos, debemos usar la técnica de sincronización para garantizar la atomicidad de la operación:

public synchronized static ClassSingleton getInstance() { if (INSTANCE == null) { INSTANCE = new ClassSingleton(); } return INSTANCE; }

Tenga en cuenta la palabra clave sincronizada en la declaración del método. El cuerpo del método tiene varias operaciones (comparación, instanciación y retorno).

En ausencia de sincronización, existe la posibilidad de que dos subprocesos intercalen sus ejecuciones de tal manera que la expresión INSTANCE == null se evalúe como verdadera para ambos subprocesos y, como resultado, se creen dos instancias de ClassSingleton .

La sincronización puede afectar significativamente el rendimiento. Si este código se invoca con frecuencia, deberíamos acelerarlo utilizando varias técnicas como la inicialización diferida o el bloqueo de doble verificación (tenga en cuenta que esto podría no funcionar como se esperaba debido a las optimizaciones del compilador). Podemos ver más detalles en nuestro tutorial "Bloqueo de doble verificación con Singleton".

Múltiples instancias

Hay varios otros problemas con los singleton relacionados con la propia JVM que podrían hacer que terminemos con varias instancias de un singleton. Estos problemas son bastante sutiles y daremos una breve descripción de cada uno de ellos:

  1. Se supone que un singleton es único por JVM. Esto podría ser un problema para los sistemas distribuidos o los sistemas cuyos componentes internos se basan en tecnologías distribuidas.
  2. Cada cargador de clases puede cargar su versión del singleton.
  3. Un singleton puede ser recolectado como basura una vez que nadie tiene una referencia a él. Este problema no provoca la presencia de varias instancias singleton a la vez, pero cuando se vuelve a crear, la instancia puede diferir de su versión anterior.

6. Conclusión

En este tutorial rápido, nos enfocamos en cómo implementar el patrón Singleton usando solo el núcleo de Java, y cómo asegurarnos de que sea consistente y cómo hacer uso de estas implementaciones.

La implementación completa de estos ejemplos se puede encontrar en GitHub.