En ocasiones necesitamos instanciar una clase solo una vez, la solución es hacer que sea la propia clase la responsable de controlar la existencia de una única instancia. Existe un patrón de diseño clásico que dicta este comportamiento: el singleton o instancia única.
Estructura, qué es un singleton
Con un diagrama UML, un singleton es algo tan simple como lo siguiente:
Podemos observar dos características básicas:
Restricción de acceso al constructor...
Con esto conseguimos que sea imposible crear nuevas instancias.Solo la propia clase puede crear la instancia.
Mecanismo de acceso a la instancia...
El acceso a la instancia única se hace a través de un único punto bien definido, que es gestionado por la propia clase y que puede ser accedido desde cualquier parte del código – en principio -
Parece sencillo. Más adelante veremos posibles extensiones que podemos aplicar al patrón inicial.
Implementación
Ahora que sabemos como es, vamos a intentar llevarlo al código de varias maneras distintas. 1. Instanciación bajo demanda
Lo que vamos a intentar en primer lugar es coger el diseño UML propuesto y llevarlo a Java directamente. Tenemos el código siguiente:
public class Singleton {
static private Singleton singleton = null;
private Singleton() { }
static public Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
/*
* Metodos del singleton.
*/
public String metodo() {
return "Singleton instanciado bajo demanda";
}
}
Esta implementación se caracteriza porque nuestra instancia única se crea cuando se usa por primera vez – bajo demanda – gracias a una sentencia condicional. En el siguiente punto eliminaremos esto, no por ser incorrecto, sino porque en general no es necesario.
Para utilizar nuestro Singleton solo tenemos que solicitar la instancia única del mismo. Habitualmente se hace de dos formas diferentes, la primera se usa en llamadas puntuales.
Singleton.getsingleton().metodo();
La segunda se usa cuando vamos a invocar nuestro Singleton varias veces, evitando llamadas innecesarias a métodos. Este tipo de singleton es un objeto como otro cualquiera, así que puede ser referenciado sin problemas.
Singleton s = Singleton.getSingleton();
s.metodo();
2. Instanciación automática
Como ya dije, la condición del método singleton() puede ser eliminada. Dicha condición se va a evaluar a cierto siempre salvo la primera vez. Además, el cargador de clases de Java, cuando referenciamos una clase no cargada anteriormente, se ocupa de cogerla de disco y cargarla en memoria inicializando sus miembros static.
Por tanto el siguiente código actúa exactamente igual que el caso 1, creando la instancia en la primera invocación pero eliminando la condición.
public class Singleton {
static private Singleton singleton = new Singleton();
private Singleton() { }
static public Singleton getSingleton() {
return singleton;
}
/*
* Metodos del singleton.
*/
public String metodo() {
return "Singleton ya instanciado";
}
}
Eliminar una condición puede ser una mejora pequeña o grande. Como siempre la optimización debe hacerse por las partes del código que se ejecutan extensivamente.
La manera de acceder al Singleton es la misma que en el caso 1, por supuesto.
3. No hay instancia
Java nos permite trabajar con clases sin necesidad de instanciarlas – posibilidad que usamos en los casos anteriores para acceder al Singleton-. Esta nueva implementación conserva la idea pero no su estructura, ya que aquí realmente no hay una instancia. El Singleton es una clase con métodos y atributos estáticos y nunca llega a existir un objeto.
public class Singleton {
static {
/*
* Inicializacion del singleton
*/
}
private Singleton() { }
/*
* Metodos del singleton.
*/
public static String metodo() {
return "Singleton estatico";
}
}
Con esa implementación, no podemos guardar referencias al objeto como en los casos anteriores – porque el objeto no existe – así que las invocaciones a métodos serán todas del tipo:
Singleton.metodo();
Una desventaja evidente es la necesidad de definir todos los métodos y atributos como static. Unas líneas más abajo comentaremos una posible extension del patrón básico para la cual esta implementación no es útil - a la hora de crear jerarquías de singletons -.
¿Qué usar?... En el fondo es una cuestión de estilo, gusto o de necesidades. Las soluciones propuestas son validas en muchos ámbitos.
Alguna extensión
El singleton, puede extenderse. Puede resultar interesante entre otras:
Que haya un número variable de instancias, seguimos controlando la cantidad, pero esta es mayor que uno.
Extender el singleton, siendo la superclase la que decida que instancia de una subclase debe ser devuelta, como puede verse en el diagrama.
Controlando la concurrencia
Hemos encontrado un mecanismo para tener una única instancia de un objeto y acceder a ella. Ahora bien, qué pasa en una aplicación multihilo, donde varios threads acceden simultáneamente al mismo espacio de memoria.
Posiblemente podremos cometer errores debido a inconsistencias de datos – dirty reads, escrituras fantasma, etc - . Por ello es necesario controlar el acceso concurrente, sincronizando los threads en ejecución.
La sincronización de threads en Java es capítulo aparte y por eso no nos extenderemos más. Únicamente sincronizaremos un Singleton y veremos como actúa. Este Singleton serializa las lecturas y escrituras de una variable. Para mostrar el bloqueo, asumiremos que la escritura tarda unos segundos – durmiendo al thread que escriba – y que no queremos que durante este proceso pueda leerse el valor.
public class Singleton {
static int valor;
static {
valor = 0;
}
private Singleton() { }
public static synchronized String getValor() {
return Integer.toString(valor);
}
public static synchronized void setValor(int nuevoValor) {
System.out.println(Thread.currentThread().getName() + " toma el bloqueo");
valor = nuevoValor;
try {
Thread.sleep(2000);
} catch (InterruptedException ie) { }
System.out.println(Thread.currentThread().getName() + " suelta el bloqueo");
}
}
Ahora generaremos dos hilos uno cambiará el valor y el otro intentará leer.
public class Main {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
Singleton.setValor(25);
}
};
t.start();
t = new Thread() {
public void run() {
System.out.println(this.getName() + " intentando leer valor");
System.out.println(this.getName() + " leido valor " + Singleton.getValor());
}
};
t.start();
}
}
Si ejecutamos obtenemos la siguiente salida:
Thread-0 toma el bloqueo
Thread-1 intentando leer valor
Thread-0 suelta el bloqueo
Thread-1 leido valor 25
Esta idea, quizás, no parece tener mucho sentido con una variable entera - a no ser que sea el balance de una cuenta corriente o algo parecido- . Pero si el obtener y el fijar implicasen cambios estructurales en una lista enlazada, por ejemplo, sin proteger el acceso concurrente podemos llegar a tener inconsistencias estructurales en la misma.
Se debe tener cuidado al serializar, pues se degrada el rendimiento. Debe buscarse el máximo paralelismo con la mínima serialización. Si llegamos al punto en que no hay trabajo en paralelo, es mejor hacer que la aplicación sea monothread.
El cargador de clases y la mentira a medias
Es ahora cuando después de explicar todo esto exponemos la verdad: los singletons en Java no existen... Tras el shock inicial, pasemos a la explicación.
Antes mencioné a grandes rasgos como funciona un cargador de clases. Quedó claro que nuestra clase Singleton se encargaba de mantener una única instancia. Sin embargo, quién dice que solo existe un cargador de clases.
De existir varios cargadores de clases, podría existir un singleton diferente en cada uno pero único en el contexto de ese cargador. Cada cargador es independiente del resto.
El lector no debería preocuparse realmente. Todo es cuestión de ser cuidadosos. Si estamos seguros de que solo hay un cargador de clases – como en una aplicación standalone común – trabajamos como hasta ahora.
Si existen varios cargadores de clases pueden suceder dos cosas. Si nuestro Singleton se usa solo para lectura de datos, no hay problema. Si lo usamos para escritura de datos debemos tener en cuenta que se pueden producir inconsistencias. Montar un “protocolo de coherencia entre singletons” es inviable – a la par que absurdo -. Lo normal en estos casos es centralizar la escritura, por ejemplo en una base de datos.
Este enfoque es habitual en aplicaciones web; entornos donde suele haber múltiples cargadores de clases. Puede haber casos donde un servidor web nos de alguna alternativa. Por ejemplo, Jakarta Tomcat proporciona una manera de compartir clases entre cargadores, sin embargo, estas soluciones son desaconsejables, pues nada nos asegura que en el futuro tengamos que cambiar nuestro entorno de ejecución, invalidando estas “peculiares” soluciones.
1 comentario:
public class Singleton
{
private Singleton() { }
private static volatile Singleton instance;
private static object lookhelper = new object();
public Singleton GetInstance()
{
if (instance == null)
{
lock (lookhelper)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
Publicar un comentario