Programación Avanzada

Patrones de diseño

Patrón de diseño Único (Singleton)

Introducción

El sistema de ficheros de mi ordenador es único.

Existe un único gestor de bases de datos para mi aplicación.

Sólo existe un único dispositivo de tipo consola.

En los tres casos anteriores debemos garantizar que sólo existe una instancia del sistema de ficheros, gestor de BBDD o dispositivo consola. Sea quien sea el cliente, accedera a los servicios del proveedor a través de la misma instancia.

En estos casos, el patrón de diseño Singleton es de mucha ayuda.

Bibliografía

Contenidos

  1. Controlar la creación de instancias.
  2. Primera prueba de nuestra implementación de Singleton.
  3. Sincroniza el acceso a los recursos compartidos.
  4. Prueba con método sincronizado.
  5. Sincronicemos sólo un bloque de código.
  6. Prueba con el bloque sincronizado.
  7. Definición del patrón de diseño Singleton.
  8. Ejemplos de uso de Singleton.
  9. Resumen.

Controlar la creación de instancias

Hasta que hemos empezado a estudiar algunos patrones de diseño, el único mecanismo que hemos utilizado para obtener una instancia de una determinada clase es el operador new.

Como recordatorio, new crea un nuevo objeto y nos devuelve una referencia a ese objeto.

Cada vez que usamos new creamos un nuevo objeto, una nueva instancia.

Controlar la creación de instancias

Como hemos visto en la introducción, hay casos en los que nos interesa que una clase sólo se pueda instanciar una única vez.

Por ejemplo, una conexión a una base de datos. Establecer una conexión a una base de datos es una operación costosa, tanto en términos de tiempo, como en términos de recursos.

Si cada vez que quiero hacer una consulta a la misma base de datos establezco una conexión, estoy consumiendo recursos de manera innecesaria.

¿Cómo puedo resolverlo? Garantizando, de algún modo, que siempre trabajo con la misma instancia de la clase que representa una conexión a la base de datos.

Controlar la creación de instancias

class Cliente {
    //1.- Obtengo una Conexión.
    //2.- Hago una Consulta a través de la Conexión.
    //3.- Me olvido de la Conexión (el último que cierre)
}
class Conexion {
    //1.- La primera vez que me piden una conexión,
    //    la creo, me la guardo y la devuelvo.
    //2.- Las siguientes veces, devuelvo la conexión que
    //    tengo guardada.
}

Controlar la creación de instancias

Los objetivos del patrón de diseño Singleton son:

  1. Prohibir que se puedan crear instancias de una clase con new.
  2. Que la clase «Única» mantenga una única instancia de sí misma.
  3. Ofrecer la posibilidad de obtener esa referencia, que es única, a todos los clientes de la clase.

Veámoslo paso por paso.

Controlar la creación de instancias

¿Cómo consigo que nadie pueda crear instancias de mí misma sin mí control?

Ocultando mis constructores.

public class Singleton {
    private Singleton() {
        super();
    }
    ...
}

Controlar la creación de instancias

¿Cómo mantengo una única instancia de mí misma?

En una referencia (estática).

public class Singleton {
    private static Singleton instancia = null;

    private Singleton() {
        super();
    }
    ...
}

Controlar la creación de instancias

¿Cómo obtienen mis clases cliente mi única instancia?

Con un método público (y estático).

public class Singleton {
    private static Singleton instancia = null;

    private Singleton() {
        super();
    }

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

Controlar la creación de instancias

Aquí tienes una clase que sólo se puede instanciar una vez:

public class Singleton {
    private static Singleton instancia = null;

    private Singleton() {
        super();
    }

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

Sencillo, quizás demasiado ;)

Primera prueba de Singleton

public class TestSingleton {
    private void ejecuta() {
        for(int i = 0; i < 100; i++)
            new Thread(() -> Singleton.getInstance()).start();
        Singleton.instanciasConcurrentes();
    }
    public static void main(String[] args) {
        new TestSingleton().ejecuta();
    }
}

Estamos creando 100 hilos que piden la instancia.

Para la creación utilizamos una expresión lambda.

Primera prueba de Singleton

new Thread(() -> Singleton.getInstance()).start();

Fíjate que ahora, no se hace uso de new para obtener una instancia de Singleton. Se hace uso de un método static que le proporciona la instancia.

Sólo leyendo este código, ¿Por qué sé que el método getInstance() es estático?

Primera prueba de Singleton

El resultado que obtenemos...

Hola soy la única instancia de Singleton: 1

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 3

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 5

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 2

...varía en cada ejecución!!!

Primera prueba de Singleton

Nuestra solución es buena en ambientes no concurrentes, pero cuando tenemos muchos clientes que piden la instancia, como has visto, puede que se creen, sin control, varias de ellas.

Afortunadamente los hilos, y su gestión, son nativos en Java. No necesitamos añadir ninguna biblioteca para trabajar con ellos.

Veamos cómo hacerlo.

Sincroniza el acceso a los recursos compartidos

public class SingletonConcurrente {
    private static SingletonConcurrente instancia = null;

    private SingletonConcurrente() {
        super();
    }

    public synchronized static SingletonConcurrente getInstance() {
        if(instancia == null)
            instancia = new SingletonConcurrente();
        return instancia;
    }
}

Fíjate en el uso de la nueva palabra reservada synchronized.

Sincroniza el acceso a los recursos compartidos

public synchronized static SingletonConcurrente getInstance() {
    if(instancia == null)
        instancia = new SingletonConcurrente();
    return instancia;
}

Un hilo que intenta acceder a un método modificado con synchronize debe obtener primero el cerrojo del objeto al que pertenece el método antes de poder ejecutarlo.

Todo objeto tiene un cerrojo, es una propiedad nativa en Java.

Si el hilo obtiene el cerrojo, puede ejecutar el método, si no, tiene que esperar a que el hilo que lo tiene en posesión lo libere.

Prueba con método sincronizado

Ahora sí que se crea una única instancia.

Hola soy la única instancia de Singleton: 1

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 1

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 1

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 1

Estupendo, lo tenemos resuelto para ambientes multi-hilo.

... pero aún lo podemos mejorar.

Prueba con método sincronizado

public synchronized static SingletonConcurrente getInstance() {
    if(instancia == null)
        instancia = new SingletonConcurrente();
    return instancia;
}

Fíjate que estamos sincronizado todo el método, pero lo que queremos evitar es que más de un hilo cree la instancia de Singleton.

Dicho de otro modo, la única instrucción conflictiva es la del new. Movamos hasta ella la sincronización.

Sincronicemos sólo un bloque de código


private static volatile SingletonConcurrenteMejorado instancia = null;

public static SingletonConcurrenteMejorado getInstance() {
    if(instancia == null)
        synchronized (SingletonConcurrenteMejorado.class) {
            if(instancia == null)
                instancia = new SingletonConcurrenteMejorado();
        }
    return instancia;
}

Ahora sólo estamos sincronizando un bloque de código, y para ello utilizamos el cerrojo de SingletonConcurrenteMejorado.class, que sí te prometo que es un objeto y único por clase ;)

Sincroniza el acceso a los recursos compartidos

private static volatile SingletonConcurrenteMejorado instancia = null;

Modificar un atributo con volatile significa que sus lecturas o escrituras se harán de manera atómica.

Si un hilo está leyendo un dato cuyo tamaño es mayor que 32 bits, se hace en un solo paso. Ningún otro hilo modificará el valor al mismo tiempo.

Sincronicemos sólo un bloque de código

public static SingletonConcurrenteMejorado getInstance() {
    if(instancia == null)
        synchronized (SingletonConcurrenteMejorado.class) {
            if(instancia == null)
                instancia = new SingletonConcurrenteMejorado();
        }
    return instancia;
}

Ahora, el primer hilo que acceda al bloque, obtendrá el cerrojo de SingletonConcurrenteMejorado.class y se creará la instancia.

Sincronicemos sólo un bloque de código

public static SingletonConcurrenteMejorado getInstance() {
    if(instancia == null)
        synchronized (SingletonConcurrenteMejorado.class) {
            if(instancia == null)
                instancia = new SingletonConcurrenteMejorado();
        }
    return instancia;
}

Cualquier hilo que llame posteriormente a este método sólo se tiene que preocupar por consultar si:

if(instancia == null)

Sin necesidad de competir por el cerrojo.

Prueba con el bloque sincronizado

Obtenemos el mismo resultado que antes.

Hola soy la única instancia de Singleton: 1

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 1

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 1

Volvemos a ejecutar y...

Hola soy la única instancia de Singleton: 1

Ahora sí que hemos terminado.

Definición del patrón de diseño Singleton

Garantiza que una clase sólo tenga una instancia, y proporciona un punto de acceso global a ella.
Patrones de diseño
Erich Gamma et al.

Definición del patrón de diseño Singleton

Este es el diagrama UML del patrón de diseño Singleton.

El diagrama UML es sencillo, la implementación, Thread-safe, no lo ha sido tanto.

Definición del patrón de diseño Singleton

Ventajas:

  1. Acceso controlado a la instancia.
  2. Evita el uso de variables globales (¿Alguien las utiliza?).
  3. En vez de una única instancia, podemos tener un número controlado de ellas.
  4. Mejor que los métodos static.

Ejemplos de uso de Singleton

Singleton es un patrón base para algunos frameworks Java, como Spring.

Las dependencias entre los componentes se inyectan como Singletons.

Esta idea se ha incorporado a la última versión de Java2 Enterprise Edition.

Tanto Spring como Java2 EE están orientados al desarrollo de aplicaciones en el lado del servidor.

Resumen

El patrón de diseño Sigleton nos sirve para controlar el número de instancias que se crean de una determinada clase.

Pertenece a la familia de patrones de diseño de creación.

De nuevo, lo que hemos encapsulado dentro de la clase Singleton es el control de la creación de instancias.

Recursos en Internet