Cómo escribir el patrón singleton correctamente
Cuando se les pide que implementen el patrón singleton, la primera reacción de muchas personas es escribir el siguiente código, incluidos los libros de texto que nos enseñan esto.
1
2
3
4
5
6 p>
6
p>
7
8
9
10
11
Clase pública Singleton {
instancia privada de Singleton estática;
privada Singleton (){}
pública estática Singleton getInstance () {
if (instancia == null ) {
instancia = new Singleton();
}
retornar instancia
p>}
}
Este código es simple y claro, y utiliza el modo de carga diferida, pero tiene un problema fatal. Cuando varios subprocesos llaman a getInstance() en paralelo, se crean varias instancias. Esto significa que no funcionará correctamente en varios subprocesos.
Perezoso, seguro para subprocesos
Para resolver el problema anterior, la forma más sencilla es sincronizar todo el método getInstance().
1
2
3
4
5
6 p>
6
p>
getInstance Singleton sincronizado estático público () {
if (instancia == null) {
instancia = new Singleton();
}
retornar instancia;
}
Aunque logra la seguridad de los subprocesos y resuelve el problema de múltiples instancias , no es eficiente. Porque solo un hilo puede llamar al método getInstance() en un momento dado. Pero la sincronización solo es necesaria en la primera llamada, cuando se crea por primera vez el objeto de instancia singleton. Esto nos lleva a cerraduras revisadas dos veces.
Bloqueo de doble verificación
El modo de bloqueo de doble verificación es un método de bloqueo mediante bloques sincronizados. Los programadores llaman a esto bloqueo de doble verificación porque habrá dos comprobaciones, por ejemplo == nulo, una fuera del bloque sincronizado y otra dentro del bloque sincronizado. ¿Por qué hacer otra verificación dentro del bloque de sincronización? Debido a que puede haber varios subprocesos ingresando al if fuera del bloque sincronizado al mismo tiempo, si la segunda verificación no se realiza dentro del bloque sincronizado, se generarán múltiples instancias.
1
2
3
4
5
6 p>
6
p>
7
8
9
10
public static Singleton getSingleton () {
if (instancia == null ) { //Verificación única
Sincronización (Singleton.class) {
if (instancia == null ) { //Doble verificación
instancia = new Singleton();
}
}
}
}}
return instancia ;
}
Este código parece perfecto, pero desafortunadamente tiene problemas. El principal problema radica en la frase instancia = new Singleton(). No es una operación atómica. De hecho, en la JVM, esta frase hace aproximadamente las siguientes tres cosas.
Asignar memoria, por ejemplo
Llamar al constructor de Singleton para inicializar las variables miembro
Apuntar el objeto de instancia al espacio de memoria asignado (después de este paso, la instancia no estar vacío)
Pero hay una optimización de reordenamiento de instrucciones en el compilador justo a tiempo de la JVM. Esto significa que el orden de los pasos 2 y 3 anteriores no está garantizado y el orden de ejecución final puede ser 1-2-3 o 1-3-2. Si es lo último, antes de ejecutar 3 pero no ejecutar 2, el subproceso 2 lo tomó y el subproceso 2 ya tiene una instancia no nula (pero no inicializada), por lo que el subproceso 2 devolverá directamente la instancia y luego usará la instancia e informar los errores de forma lógica.
Simplemente declaramos la variable de instancia como volátil.
1
2
3
4
5
6 p>
6
p>
7
8
9
10
11
12
13
14
15
16
clase pública Singleton {
instancia Singleton estática volátil privada; //declarada volátil
Singleton privada (){}
Singleton estática pública getSingleton () {
if (instancia == null ) { p>
sincronizado (Singleton.class) {
if (instancia == null ) {
instancia = nuevo Singleton; //declarado volátil
singleton privado () {
Singleton privado () {instancia = new Singleton();
}
}
} p>
}
instancia de retorno;
}
}
}
Algunas personas piensan que la razón de volátil es la visibilidad, lo que significa que puede asegurarse de que el hilo no tenga una copia de la instancia localmente. La instancia volátil se lee desde la memoria principal cada vez. tiempo. Pero ese no es el caso. La razón principal para usar volátil es otra característica: deshabilitar las optimizaciones de reordenamiento de instrucciones. Es decir, después de una asignación a una variable volátil, hay una barrera de memoria (en el código ensamblador generado) y las lecturas no se reordenan antes de la barrera de memoria. Por ejemplo, en el ejemplo anterior, la operación de valor debe ejecutarse después de 1-2-3 o 1-3-2. No existe una situación en la que 1-3 se ejecute primero y luego se obtenga el valor. Según el principio de "primero en llegar, primero en ser atendido", una operación de escritura en una variable volátil ocurre antes de una operación de lectura en la variable (el "después" aquí es el orden cronológico).
Pero tenga en cuenta en particular que el uso de volátil para el bloqueo de doble verificación sigue siendo un problema en versiones anteriores a Java 5. La razón es que existe una falla en el JMM (Java Memory Model) anterior a Java 5. Incluso declarar una variable como volátil no puede evitar completamente el reordenamiento, principalmente porque el código antes y después de la variable volátil aún se verá afectado por el reordenamiento. El problema del reordenamiento del bloqueo de volátiles solo se resolvió en Java 5, por lo que después de Java 5 puedes usar volátiles con confianza.
Estoy seguro de que no le gustará este enfoque complicado y potencialmente problemático, y ciertamente hay mejores formas de implementar un patrón singleton seguro para subprocesos.
Campo final estático hambriento
Este enfoque es muy simple ya que la instancia del singleton se declara como una variable estática y final y se inicializa cuando la clase se carga por primera vez en la memoria, creando así la instancia es inherentemente segura para subprocesos.
1
2
3
4
5
6 p>
6
p>
7
8
9
10
public class Singleton {
/// Inicializado cuando se carga la clase
instancia Singleton final estática privada = new Singleton();
Singleton privado (){}
getInstance Singleton estático público () {
instancia de retorno;
}
}
Si esto fuera Perfecto, no habría necesidad de todas las tonterías del bloqueo de doble control. La desventaja de esta forma de escribir es que no es un modo de inicialización diferida. En este modo, incluso si el cliente no llama al método getInstance (), el singleton se inicializará inmediatamente después de cargar la clase. En algunos casos, este enfoque de inanición no funcionará: por ejemplo, si un singleton depende de parámetros o de un archivo de configuración, y se debe llamar a un método antes de getInstance() para establecer sus parámetros, entonces este enfoque de singleton no funcionará.
Clases internas estáticas Clases anidadas estáticas
Prefiero el enfoque de clases internas estáticas, que también es el enfoque recomendado en Effective Java.
1
2
3
4
5
6 p>
6
p>
7
8
9
clase pública Singleton {
Clase estática privada SingletonHolder {
INSTANCIA Singleton final estática privada = new Singleton();
}
Singleton privada (){}
public static final Singleton getInstance () {
return SingletonHolder.INSTANCE;
}
}
Este método aún utiliza el propio mecanismo de la JVM para garantizar la seguridad de los subprocesos; debido a que SingletonHolder es privado, no hay otra forma de acceder a él excepto getInstance (), por lo que es lento y no hay sincronización al leer la instancia, lo que no trae inconvenientes de rendimiento; y no depende de la versión JDK.
Enumeración Enumeración
¡Escribir singletons usando enumeraciones es muy fácil! Ésta es su mayor ventaja. El siguiente código es la forma normal de declarar una instancia de enumeración.
1
2
3
enumeración pública EasySingleton{
INSTANCIA;
}
Podemos acceder a la instancia a través de EasySingleton.INSTANCE, que es mucho más simple que llamar al método getInstance(). La creación de enumeraciones es segura para subprocesos de forma predeterminada, por lo que no tiene que preocuparse por el bloqueo de doble verificación y también evita que la deserialización provoque la recreación de nuevos objetos. Pero pocas personas escriben de esta manera, probablemente porque no están familiarizadas con esta forma de escribir.
Resumen
En términos generales, hay cinco formas de escribir patrones singleton: perezoso, hambriento, bloqueo de doble verificación, clases internas estáticas y enumeraciones. Todos los métodos anteriores son implementaciones seguras para subprocesos, mientras que el primer método proporcionado al principio del artículo no se considera la forma correcta de escribirlo.