Modo singleton de DCL

El llamado DCL es Double Check Lock, es decir, control de doble bloqueo Antes de entender cómo se aplica DCL en el modo singleton, primero comprendamos el modo singleton. El modo singleton generalmente se divide en "hombre hambriento" y "hombre vago", comienza con simple

Hombre hambriento

El llamado "hombre hambriento" se debe a que la instancia se crea cuando se inicia el programa.En términos sencillos, significa que la comida se acaba de servir y todos comen un bocado antes de comenzar a comer.

public class Singleton {
    
    
    private static final Singleton singleton = new Singleton();
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        return singleton;
    }
}

La línea 3 restringe la forma de crear tales objetos a través de un constructor privado (ignorado por reflexión). Este método es muy seguro, pero es un desperdicio de recursos hasta cierto punto. Por ejemplo, una instancia de Singleton se crea desde el principio, pero rara vez se usa. Esto provoca un desperdicio de recursos en el área del método y, por lo tanto, otra instancia de Singleton modo, es decir, modo singleton hombre perezoso

Hombre flojo

La razón por la que se le llama "hombre vago" es porque sólo aparece cuando realmente lo llama. Si no lo llama, lo ignorará y no tiene nada que ver con eso. En otras palabras, la instancia se crea cuando se usa realmente, no al principio. Como se muestra en el siguiente código:


public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        if(null == singleton){
    
    
            singleton = new Singleton();
        }
        return singleton;
    }
}

Parece una pieza de código muy simple, pero hay un problema, es decir, la inseguridad del hilo. Por ejemplo, ahora hay 1000 subprocesos, todos los cuales necesitan esta instancia de Singleton, para verificar si se obtiene la misma instancia, el código es el siguiente:

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        if(null == singleton){
    
    
            try {
    
    
                Thread.sleep(1);//象征性的睡了1ms
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            singleton = new Singleton();
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

Parte de los resultados de ejecución son confusos:

944436457
1638599176
710946821
67862359

¿Por qué está pasando esto? El primer hilo llegó, se ejecutó en la línea 7 y se durmió durante 1 ms. Mientras dormía, llegó el segundo hilo. Cuando el segundo hilo se ejecuta en la línea 5, el resultado debe estar vacío, por lo que habrá Dos hilos que crean un objeto cada uno, lo que inevitablemente conducirá a Singleton.getInstance().hashCode()resultados inconsistentes. Se puede mejorar de la siguiente manera agregando un candado a todo el método:

Mejora 1

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static synchronized Singleton getInstance(){
    
    
        if(null == singleton){
    
    
            try {
    
    
                Thread.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            singleton = new Singleton();
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

El problema de la consistencia del hilo se resuelve agregando sincronizado al método getInstance (). El análisis de resultados muestra que los códigos hash de todas las instancias son iguales, pero la granularidad de sincronizados es demasiado grande, es decir, el área crítica del bloqueo es demasiado grande, lo que afecta un poco la eficiencia, por ejemplo, si hay una lógica de procesamiento de negocios entre las líneas 4 y 5, que no involucrará variables compartidas, por lo que cada vez que esta parte de la lógica de negocios se bloquee, inevitablemente conducirá a la ineficiencia . Para resolver el problema general, el código se puede mejorar aún más:

Mejora 2

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        /*
        一堆业务处理代码
         */
        if(null == singleton){
    
    
            synchronized(Singleton.class){
    
    //锁粒度变小
                try {
    
    
                    Thread.sleep(1);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                singleton = new Singleton();
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

Parte de los resultados de la ejecución:

391918859
391918859
391918859
1945023194

Al analizar los resultados de la ejecución, se descubre que, aunque la granularidad del bloqueo se ha reducido, el hilo no es seguro. ¿Por qué esto es tan? Debido a que hay una situación, el intervalo de tiempo se agota cuando el hilo 1 termina de ejecutar el juicio if y no ha obtenido el bloqueo. En este momento, está llegando el hilo 2. Al ejecutar el juicio if, se encuentra que el objeto aún está vacío. Continuar ejecutando la ejecución. Se alcanza el bloqueo, por lo que el hilo 2 crea un objeto. Cuando se crea el hilo 2, el bloqueo se libera. En este momento, el hilo 1 se activa, obtiene con éxito el bloqueo y crea un objeto. Entonces, el código necesita otro paso de mejora.

Mejora 3

public class Singleton {
    
    
    private static Singleton singleton = null;
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        /*
        一堆业务处理代码
         */
        if(null == singleton){
    
    
            synchronized(Singleton.class){
    
    //锁粒度变小
                if(null == singleton){
    
    //DCL
                    try {
    
    
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
    
    
        for (int i=0;i<1000;i++){
    
    
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

Al agregar otra capa de juicio if en la línea 10, es el llamado Double Check Lock. Es decir, incluso si obtiene el bloqueo, debe emitir un juicio. Si el objeto de juicio no está vacío en este momento, entonces no necesita crear el objeto nuevamente y simplemente regresar directamente. Esto resuelve el problema en el problema "Mejora 2". Pero, ¿es posible ir a la línea 8 aquí? Personalmente, creo que está bien. Mantener la línea 8 es para mejorar la eficiencia, porque si vas, cada hilo agarrará directamente el candado cuando pase. El agarre del candado en sí afectará la eficiencia El juicio if es solo de unos pocos ns, y la mayoría de los hilos no necesitan agarrar el candado, por lo que es mejor mantenerlo.
Hasta ahora, se ha introducido el principio de DCL singleton, pero todavía existe un problema. Es necesario considerar el tema del reordenamiento de instrucciones, por lo que se deben agregar volátiles para prohibir el reordenamiento de instrucciones. Continúe analizando el código y simplifique el código Singleton para facilitar el análisis:

public class Singleton {
    
    
    int a = 5;//考虑指令重排序的问题
}

singleton = new Singleton()El código de bytes es el siguiente:

  0: new    #2           // class com/reasearch/Singleton
  3: dup
  4: invokespecial #3   // Method com/reasearch/Singleton."<init>":()V
  7: astore_1

Independientemente del comando dup. Agregue un punto de conocimiento aquí. Al crear un objeto, asigne espacio primero, y las variables en la clase primero tendrán un valor predeterminado, y luego asigne un valor a la variable después de que se llame al constructor. Por ejemplo int a = 5, a = 0 al principio. El proceso de ejecución de las instrucciones de código de bytes es el siguiente,

  1. nuevo asigna espacio, a = 0
  2. invoca método de construcción especial a = 5
  3. astore_1 asigna el objeto a singleton

Este es el estado ideal. 2 y 3 no tienen conexión semántica y lógica, por lo que JVM puede permitir que estas instrucciones se ejecuten fuera de orden, es decir, ejecutar 3 primero y luego ejecutar 2. Volviendo a la mejora 3 , si el hilo 1 vuelve a ejecutar la línea 16 de código, el orden de ejecución de las instrucciones es 1, 3, 2. Cuando se ejecuta 3, se agota el intervalo de tiempo, en este momento a = 0, que significa que la inicialización está a mitad de camino. En este momento, viene el hilo 2 y la octava línea juzga que el singleton definitivamente no está vacío, por lo que devuelve directamente un objeto Singleton, pero de hecho este objeto es un objeto problema, un objeto semiinicializado, es decir a=0. Esto se debe al reordenamiento de las instrucciones, por lo que para evitar este fenómeno, agregue la palabra clave volatile. Por tanto, la versión final del código del modo singleton de DCL es la siguiente:

versión completa

public class Singleton {
    
    
    private volatile static Singleton singleton = null;//加上volatile 
    private Singleton(){
    
    }
    public static Singleton getInstance(){
    
    
        /*
        一堆业务处理代码
         */
        if(null == singleton){
    
    
            synchronized(Singleton.class){
    
    //锁粒度变小
                if(null == singleton){
    
    //DCL
                    try {
    
    
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

En este punto, podemos llegar a su fin. Creo que muchos socios pequeños escribirán singletons, pero todavía es difícil entender los principios. ¡Alegrémonos!

Supongo que te gusta

Origin blog.csdn.net/hongyinanhai00/article/details/113971787
Recomendado
Clasificación