El patrón de Doble Verificación Bloqueada (o DCL por sus siglas en inglés) es una implementación errónea que intenta optimizar la inicialización perezosa de instancias únicas (singletons) para evitar la sobrecarga de sincronización en cada acceso. Una implementación correcta, aunque sincrónica, sería:
public class FabricaCorrecta {
private static FabricaCorrecta instanciaUnica;
public static synchronized FabricaCorrecta obtenerInstancia() {
if (instanciaUnica == null) {
instanciaUnica = new FabricaCorrecta();
}
return instanciaUnica;
}
private FabricaCorrecta() {
// Inicialización...
}
}
El problema surge cuando se intenta optimizar esta solución, eliminando la sincronización en la lectura externa y dejándola solo en la sección crítica interna:
public class FabricaRota {
private static FabricaRota instanciaUnica;
private int campo1, campo2; // Campos de ejemplo
public static FabricaRota obtenerInstancia() {
// ¡Esto es incorrecto! No lo intentes.
if (instanciaUnica == null) {
synchronized (FabricaRota.class) {
if (instanciaUnica == null) {
instanciaUnica = new FabricaRota();
}
}
}
return instanciaUnica;
}
private FabricaRota() {
// Inicialización de campos...
campo1 = 1;
campo2 = 2;
}
}
Esta implementación es incorrecta debido a las garantías de ordenación de memoria y visibilidad entre hilos en versiones de Java anteriores a la 5. Un hilo podría ver una referencia a un objeto recién creado, pero sus campos internos aún no habrían sido completamente inicializados por el hilo creador, llevando a inconsistencias.
Soluciones para el DCL defectuoso:
1. Sincronización Sencilla
La forma más directa y robusta, especialmente en la actualidad, es utilizar la sincronización en el método de acceso. La sobrecarga percibida de la sincronización en accesos no disputados es mínima en las JVMs modernas.
public class FabricaSimple {
private static FabricaSimple instanciaUnica;
public static synchronized FabricaSimple obtenerInstancia() {
if (instanciaUnica == null) {
instanciaUnica = new FabricaSimple();
}
return instanciaUnica;
}
private FabricaSimple() {}
}
2. Inicialización Estática (Cargador de Clases)
Una de las formas más eficientes y seguras de manejar la inicialización perezosa de singletons es confiar en el cargador de clases de Java. La inicialización de campos estáticos o bloques estáticos se garantiza que ocurre exactamente una vez de forma atómica cuando la clase es cargada por primera vez.
Opción A: Inicialización directa
public class FabricaCargador {
private static final FabricaCargador instanciaUnica = new FabricaCargador();
public static FabricaCargador obtenerInstancia() {
return instanciaUnica;
}
private FabricaCargador() {}
}
Opción B: Bloque estático (para manejo de excepciones)
public class FabricaCargadorConExcepcion {
private static final FabricaCargadorConExcepcion instanciaUnica;
static {
try {
instanciaUnica = new FabricaCargadorConExcepcion();
} catch (Exception e) {
// Manejo de error de inicialización
throw new RuntimeException("Error al inicializar la fábrica", e);
}
}
public static FabricaCargadorConExcepcion obtenerInstancia() {
return instanciaUnica;
}
private FabricaCargadorConExcepcion() throws Exception {
// Lógica de inicialización que puede lanzar excepciones
}
}
Este enfoque es generalmente preferido por su simplicidad y eficiencia, a menos que el método de obtención necesite aceptar parámetros.
3. DCL con volatile (Java 5+)
A partir de Java 5, la palabra clave volatile se actualizó para garantizar que las lecturas y escrituras de una varible volátil tengan semántica de ordenación específica. Esto hace que el patrón DCL sea seguro si la variable de instancia se declara como volatile.
import java.io.IOException;
import java.sql.Connection;
public class FabricaVolatile {
private static volatile FabricaVolatile instanciaUnica;
public static FabricaVolatile obtenerInstancia(Connection conn) throws IOException {
if (instanciaUnica == null) {
synchronized (FabricaVolatile.class) {
if (instanciaUnica == null) {
instanciaUnica = new FabricaVolatile(conn);
}
}
}
return instanciaUnica;
}
private FabricaVolatile(Connection conn) throws IOException {
// Inicialización usando la conexión
}
}
La declaración volatile asegura que cualquier escritura a instanciaUnica sea visible inmediatamente para otros hilos y que las lecturas posteriores vean el estado completo y correcto del objeto.
4. Campos final
En Java 5 y posteriores, los campos declarados como final en un constructor se garantizan que sus valores se escriben en memoria principal antes de que la referencia al objeto sea visible para otros hilos. Esto significa que, si todos los campos de la instancia se inicializan en el constructor y se marcan como final, la instancia no necesitaría ser volatile para garantziar la visibilidad correcta de sus campos.
public class FabricaFinal {
private static FabricaFinal instanciaUnica;
public static FabricaFinal obtenerInstancia() {
// Si la inicialización se hace con DCL, y los campos son final,
// este DCL es seguro sin volatile. Sin embargo, la inicialización estática es más simple.
if (instanciaUnica == null) {
synchronized (FabricaFinal.class) {
if (instanciaUnica == null) {
instanciaUnica = new FabricaFinal();
}
}
}
return instanciaUnica;
}
private final int valorConstante1;
private final String valorConstante2;
private FabricaFinal() {
this.valorConstante1 = 100;
this.valorConstante2 = "Constante";
// La garantía de final asegura que estos valores son visibles
// junto con la referencia a la instancia.
}
}