Mejores prácticas del patrón de diseño Singleton en Java con ejemplos

Introducción

El Patrón Singleton en Java es uno de los Patrones de Diseño de las Cuatro Bandas y pertenece a la categoría de Patrones de Diseño Creacionales. Desde la definición, parece ser un patrón de diseño sencillo, pero cuando se trata de la implementación, surgen muchas preocupaciones.

En este artículo, aprenderemos sobre los principios del patrón de diseño singleton, exploraremos diferentes formas de implementar el patrón de diseño singleton y algunas de las mejores prácticas para su uso.

Principios del Patrón Singleton

  • El patrón singleton restringe la instanciación de una clase y asegura que solo exista una instancia de la clase en la Máquina Virtual de Java.
  • La clase singleton debe proporcionar un punto de acceso global para obtener la instancia de la clase.
  • El patrón singleton se utiliza para el registro, objetos de controladores, almacenamiento en caché y grupo de hilos.
  • El patrón de diseño Singleton también se utiliza en otros patrones de diseño como Abstract Factory, Builder, Prototype, Facade, etc.
  • El patrón de diseño Singleton también se utiliza en las clases principales de Java (por ejemplo, java.lang.Runtime, java.awt.Desktop).

Implementación del Patrón Singleton en Java

Para implementar un patrón Singleton, existen diferentes enfoques, pero todos ellos tienen los siguientes conceptos comunes.

  • Constructor privado para restringir la instanciación de la clase desde otras clases.
  • Variable estática privada de la misma clase que es la única instancia de la clase.
  • Método estático público que devuelve la instancia de la clase; este es el punto de acceso global para que el mundo exterior obtenga la instancia de la clase Singleton.

En las secciones siguientes, aprenderemos diferentes enfoques para la implementación del patrón Singleton y las consideraciones de diseño asociadas.

1. Inicialización ansiosa

En la inicialización ansiosa, la instancia de la clase singleton se crea en el momento de la carga de la clase. La desventaja de la inicialización ansiosa es que el método se crea incluso si la aplicación cliente puede que no lo esté utilizando. Aquí está la implementación de la clase singleton de inicialización estática:

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // constructor privado para evitar que las aplicaciones cliente usen el constructor
    private EagerInitializedSingleton(){}

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

Si su clase singleton no está utilizando muchos recursos, este es el enfoque a usar. Pero en la mayoría de los escenarios, las clases singleton se crean para recursos como el sistema de archivos, conexiones de bases de datos, etc. Deberíamos evitar la instanciación a menos que el cliente llame al método getInstance. Además, este método no proporciona opciones para el manejo de excepciones.

2. Inicialización mediante bloque estático

La implementación de inicialización de bloque estático es similar a la inicialización ansiosa, excepto que la instancia de la clase se crea en el bloque estático que proporciona la opción para el manejo de excepciones.

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // inicialización de bloque estático para el manejo de excepciones
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

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

Tanto la inicialización ansiosa como la inicialización de bloque estático crean la instancia incluso antes de que se utilice y esa no es la mejor práctica para usar.

3. Inicialización Perezosa

El método de inicialización perezosa para implementar el patrón singleton crea la instancia en el método de acceso global. Aquí tienes un ejemplo de código para crear la clase singleton con este enfoque:

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

La implementación anterior funciona bien en el caso del entorno de un solo hilo, pero cuando se trata de sistemas multinúcleo, puede causar problemas si múltiples hilos están dentro de la if condición al mismo tiempo. Destruirá el patrón singleton y ambos hilos obtendrán instancias diferentes de la clase singleton. En la próxima sección, veremos diferentes formas de crear una clase singleton segura para subprocesos.

4. Hilo Seguro Singleton

A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

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

}

La implementación anterior funciona bien y proporciona seguridad en el hilo, pero reduce el rendimiento debido al costo asociado con el método sincronizado, aunque solo lo necesitamos para los primeros hilos que podrían crear instancias separadas. Para evitar este gasto adicional cada vez, se utiliza el principio de bloqueo de doble verificación. En este enfoque, el bloque sincronizado se utiliza dentro de la condición if con una verificación adicional para asegurar que solo se cree una instancia de una clase singleton. El siguiente fragmento de código proporciona la implementación de bloqueo de doble verificación:

public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

Continúa tu aprendizaje con Clase Singleton Segura para Hilos.

5. Implementación Singleton de Bill Pugh

Antes de Java 5, el modelo de memoria de Java tenía muchos problemas, y los enfoques anteriores solían fallar en ciertos escenarios donde demasiados hilos intentaban obtener la instancia de la clase singleton simultáneamente. Así que Bill Pugh ideó un enfoque diferente para crear la clase singleton utilizando una clase auxiliar interna estática. Aquí tienes un ejemplo de la implementación del Singleton de Bill Pugh:

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Observa la clase interna privada estática que contiene la instancia de la clase singleton. Cuando se carga la clase singleton, la clase SingletonHelper no se carga en la memoria y solo cuando alguien llama al método getInstance(), esta clase se carga y crea la instancia de la clase singleton. Este es el enfoque más ampliamente utilizado para la clase singleton, ya que no requiere sincronización.

6. Uso de Reflexión para destruir el Patrón Singleton

La reflexión se puede utilizar para destruir todos los enfoques anteriores de implementación singleton. Aquí tienes un ejemplo de clase:

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // Este código destruirá el patrón singleton
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

Cuando ejecutas la clase de prueba anterior, notarás que el hashCode de ambas instancias no es el mismo, lo que destruye el patrón singleton. La reflexión es muy poderosa y se utiliza en muchos frameworks como Spring e Hibernate. Continúa tu aprendizaje con el Tutorial de Reflexión en Java.

7. Singleton Enum

Para superar esta situación con la reflexión, Joshua Bloch sugiere el uso de enum para implementar el patrón de diseño singleton, ya que Java asegura que cualquier valor de enum se instancie solo una vez en un programa de Java. Dado que los valores de Enum de Java son globalmente accesibles, también lo es el singleton. La desventaja es que el tipo enum es algo inflexible (por ejemplo, no permite la inicialización perezosa).

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // hacer algo
    }
}

8. Serialización y Singleton

A veces, en sistemas distribuidos, necesitamos implementar la interfaz Serializable en la clase singleton para poder almacenar su estado en el sistema de archivos y recuperarlo en un momento posterior. Aquí hay una pequeña clase singleton que también implementa la interfaz Serializable:

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}

El problema con una clase singleton serializada es que cada vez que la deserializamos, crea una nueva instancia de la clase. Aquí tienes un ejemplo:

package com.journaldev.singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        // deserializar de archivo a objeto
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}

Ese código produce esta salida:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

Así que destruye el patrón singleton. Para superar este escenario, todo lo que necesitamos hacer es proporcionar la implementación del método readResolve().

protected Object readResolve() {
    return getInstance();
}

Después de esto, notarás que el hashCode de ambas instancias es el mismo en el programa de prueba.

Lee acerca de la Serialización en Java y la Deserialización en Java.

Conclusión

Este artículo cubrió el patrón de diseño singleton.

Continúa tu aprendizaje con más tutoriales de Java.

Source:
https://www.digitalocean.com/community/tutorials/java-singleton-design-pattern-best-practices-examples