¿Por qué las cerraduras de doble verificación necesitan usar la palabra clave volátil?

Descripción del frente

Esta es mi propia comprensión de los problemas de usar bloqueos comprobados dos veces y el papel de la semántica volátil en ellos.
Es un artículo documental personal, que puede ser un poco sencillo en la descripción del idioma, y ​​no citará demasiadas explicaciones o explicaciones de terminología técnica oficial.
Si hay algún problema, deje un mensaje para corregirlo.
Empiece el texto a continuación:

Una implementación singleton

Si hay una clase Earth (Tierra), debemos proporcionar una implementación perezosa de singleton. Una forma convencional de escritura es la siguiente:

public class Earth {
    
    
    private static Earth earth = null;

    public static Earth getInstance() {
    
    
        if (earth == null) {
    
    
            earth = new Earth();
        }
        return earth;
    }
}

En un entorno de un solo subproceso, este tipo de escritura está completamente libre de problemas, no es necesario considerar problemas de concurrencia y no habrá secciones críticas.
Desafortunadamente, en realidad, las CPU de nuestra computadora son generalmente de múltiples núcleos, y los programas a menudo necesitan ejecutarse simultáneamente con múltiples subprocesos. En un entorno de este tipo, si se ejecutan varios subprocesos en este método al mismo tiempo, este código puede crear varios objetos nuevos y asignar memoria repetidamente. De manera similar, el modo singleton puede ir acompañado de una inicialización lógica de negocios compleja en aplicaciones prácticas, y esta inicialización repetida no estará permitida.
Entonces la solución es sincronizar las operaciones. Por ejemplo: bloquear este método

    public static synchronized Earth getInstance() {
    
    
        if (earth == null) {
    
    
            earth = new Earth();
        }
        return earth;
    }

Por supuesto, sin considerar el rendimiento, este código debe garantizar la seguridad del subproceso. Cuando el subproceso que obtiene el bloqueo ingresa primero a este método de sincronización, si se encuentra que el objeto terrestre está vacío, construirá un objeto y lo asignará, y luego otro Después el hilo entra, el objeto ha sido creado. De acuerdo con la semántica de sincronizado, no habrá ningún problema aquí.
Sin embargo, en el proceso de desarrollo real, se espera que la granularidad del bloqueo sea lo más pequeña posible para reducir el tiempo de bloqueo de la espera del bloqueo. Por lo tanto, la operación sincronizada se coloca en el cuerpo del método para sincronizar parte del código. Eso es lo que dije a continuación, verifique dos veces el bloqueo.

Cerradura de doble verificación

Un ejemplo de código incorrecto:

public class Earth {
    
    
    private static Earth earth = null;

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

Este es un bloqueo de doble verificación. Primero determine si la tierra está vacía, si está vacía, ingrese al bloque de sincronización, y luego determine si está vacía y todavía vacía, luego se le da un nuevo objeto. De esta forma, cuando otros subprocesos ingresen al cuerpo de este método, si encuentran que el objeto ha sido creado y no está vacío, no serán bloqueados esperando en el bloque de sincronización, sino que ejecutarán directamente el siguiente código. el siguiente código en el ejemplo no tiene lógica. Simplemente regrese a la instancia de tierra directamente.
El objeto de bloqueo del bloque de sincronización ps no se recomienda para usar Earth.class, es decir, el bloqueo del objeto de clase de la clase pertenece, el rango es demasiado grande, de hecho, el bloque de sincronización del método estático también puede usar este . Es mejor crear un objeto nuevo como objeto de bloqueo.
Por supuesto, el ejemplo de código anterior es incorrecto, porque el atributo de tierra declarado no usa la palabra clave volátil. Por lo tanto, hay un problema con este bloqueo de verificación doble. ¿Por qué hay un problema? A continuación se explica.

Lo he usado tan mal

Cuando me gradué, no sabía lo suficiente sobre algunas de las especificaciones de Java. El bloqueo de doble verificación se escribió de la manera anterior. Por ejemplo, al declarar el atributo de tierra, no sabía cómo usar volatile. Un día, cuando estaba escribiendo código, un estudiante de último año me vio escribiendo cerraduras de doble verificación y me dijo que la palabra clave volátil debería agregarse para declarar las propiedades del objeto singleton que se creará. Solo pregunté por qué. Me dijo que debería evitar "reordenar" términos técnicos que no podía entender en ese momento. No me conté más, solo dije que he visto el problema de reordenar algunos materiales. Lo que dije fue dudar, ¡y también estaba desconcertado por lo que escuché!
En épocas posteriores, también leí muchos materiales o libros relacionados con JVM. Por supuesto, como el tiempo ha pasado durante tanto tiempo, no puedo recordar el contenido específico de los libros que leí durante ese tiempo. Solo recuerde escribir un bloqueo de doble verificación para agregar una declaración volátil.

Paradigma de escritura correcta

public class Earth {
    
    
    private static volatile Earth earth = null;

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

Respecto a los volátiles, siempre recuerdo dos términos relacionados con ellos: visibilidad y prohibición de reordenar.
Entonces, ¿cuáles son exactamente la visibilidad y la prohibición de reordenar en semántica volátil?

volátil

Con respecto a la "visibilidad" y el "reordenamiento", para algunos estudiantes que no están bien fundamentados, es posible que no entiendan lo que quieren decir. Aquí hay algunas explicaciones simples, que pueden no ser profesionales, pero trate de ser lo más directo posible y espere que sean fáciles de entender.
Tres problemas comunes en la programación concurrente: visibilidad, atomicidad y orden.

  • Los
    términos atomicidad deben escucharse con frecuencia. La atomicidad no es el tema de este artículo y no está en el alcance de la discusión. Si necesita comprender en detalle, puede encontrar información relevante. En pocas palabras: nuestra línea de código en Es posible que sea necesario explicar java. Para que la CPU ejecute varias instrucciones de máquina (por ejemplo: num + = 1), estas instrucciones pueden cambiar de hilo para ejecutar otras declaraciones en cualquier momento durante el proceso de ejecución, y esta instrucción no se ha ejecutado sin embargo, que es un problema de atomicidad. A menos que estas instrucciones no se interrumpan durante la ejecución de la CPU, es atómico.
  • Visibilidad
    Se trata de la visibilidad de la memoria. Por ejemplo, hay una variable:
int num = 0;

Hay subproceso A y subproceso B, el subproceso A establece la variable num = 1, y luego cuando el subproceso B lee el valor de num, el valor leído sigue siendo 0. Después de la operación de lectura de B ocurre la operación de escritura de A, pero B lee No es el 1 que acaba de escribir A. ¿Podría existir este problema? Si, existe.
Este es un problema de coherencia causado por el almacenamiento en caché de hardware. Entonces, existe JMM (modelo de memoria Java) para resolver este problema de coherencia de caché.
En el JMM del que hablamos a menudo, se mencionará un término, llamado memoria local (caché local) del hilo de Java, que tiene cada hilo. Hay muchos materiales, y también se dice que al ejecutar código en un hilo, cuando se lee el valor de una variable, primero se cargará de la memoria principal a la memoria local. Después de modificar el valor de la variable, se modifica la memoria local y el valor de se volverá a escribir en la memoria principal. Si antes de volver a escribir en la memoria principal, si otro hilo lee el valor de esta variable, otro hilo, por supuesto, no podrá obtener el último valor actualizado en la memoria local de este hilo, porque independientemente de si el otro hilo es de propia La lectura de la memoria local o la memoria principal no es el último valor, que es el problema de visibilidad de la memoria mencionado anteriormente.
Cuando entré por primera vez en contacto con el concepto de memoria local, en realidad no entendía muy bien qué es esta memoria local, y muchos estudiantes novatos también deberían serlo.
Esta memoria principal es nuestra memoria física.
Esta memoria local es en realidad un concepto abstracto en Java, incluye caché de CPU, registros u otro hardware, optimización de compilación, etc. No es un espacio de memoria abierto por cada hilo por separado. Sin embargo, JMM en realidad analiza el concepto abstracto de memoria local.
Por lo tanto, cada vez que Java lee el valor de una variable, primero se lee de la caché local y luego se carga desde la memoria principal a la memoria local si no es así. La escritura también se escribe aquí primero y luego se sincroniza con la memoria principal. memoria.
La visibilidad de volátiles garantiza que cada lectura se obtiene de la memoria principal y se sincroniza con la memoria principal después de la escritura, de modo que cada hilo puede ver el último valor actualizado por otros hilos.

  • Orden El
    problema del orden es realmente contrario a la intuición . Creemos que el código que se ejecuta secuencialmente debe ejecutarse de arriba hacia abajo y de adelante hacia atrás, a veces el resultado no es el esperado.
    La razón es el problema de reordenamiento causado por la optimización del compilador, el tiempo de ejecución o la CPU .
    A continuación, se muestran varios reordenamientos posibles:
  1. Reordenamiento del compilador o del tiempo de ejecución, como el siguiente :, el a = 1; b = 2;orden compilado puede ser: b = 2; a = 1;(Aquí hay una analogía, por no decir que el reordenamiento de compilado en código de bytes).
  2. Reordenamiento de hardware, como reordenar cuando la CPU ejecuta la optimización de las instrucciones de la máquina
  3. La reordenación del sistema de almacenamiento. Esto se debe a que los datos se escriben primero en la caché, no se actualizan inmediatamente en la memoria principal. Existen condiciones y condiciones correspondientes, lo que no garantiza qué subproceso escribirá el valor de qué variable primero en el caché., Primero se actualizará a la memoria principal.
  4. Reordenar en otros casos.
    Este problema de reordenación de los bloqueos de doble verificación se refiere al segundo tipo, un problema de reordenación de las instrucciones de la máquina de la CPU.

Reordenación de instrucciones de bloqueo de doble verificación

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

Este es el código para crear el objeto con el bloqueo de doble verificación. Si dos subprocesos A y B ingresan a este método y encuentran que el objeto terrestre está vacío, comienzan a competir por el bloqueo. El subproceso A obtiene el bloqueo y entra en la sincronización bloque, y el hilo B espera el bloqueo. Después de que A crea el objeto y libera el bloqueo, B obtiene el bloqueo y descubre que la tierra no está vacía, por lo que sale del bloque de sincronización y regresa a la instancia de tierra. Esto no es un problema.
Sin embargo, earth = new Earth();esta línea de código tiene múltiples operaciones:

  1. Asignar espacio de memoria
  2. Inicializar el objeto en esta memoria
  3. Asignar la dirección de memoria a la variable de tierra

Después de reordenar:

  1. Asignar espacio de memoria
  2. Asignar la dirección de memoria a la variable de tierra
  3. Inicializar el objeto en esta memoria

Si el subproceso A realiza la segunda acción y la tierra no está vacía en este momento, el subproceso A ingresa a este método para determinar si la tierra está vacía y encuentra que no está vacía, y luego devuelve un objeto que aún no se ha inicializado. y sus propiedades pueden estar todavía. Todos están vacíos. Si accede a las propiedades de miembro de esta variable en este momento, puede haber problemas, como una excepción de puntero nulo.
La siguiente imagen es una captura de pantalla de una prueba similar que encontré para ilustrar este problema. La información relevante está vinculada al final:
Inserte la descripción de la imagen aquí

Qué tan volátil prohíbe reordenar

En la actualidad, en jdk1.5 a 1.8, solo la semántica de volatile debería incluir prohibir el reordenamiento de instrucciones. Sincronizado tampoco es compatible, solo garantiza el orden y la atomicidad de los bloques sincronizados. Su orden se debe a que solo un subproceso ejecuta el código en la sección crítica en un momento en que el bloqueo está bloqueado, y esta atomicidad significa que el bloque de código bloqueado es externamente atómico, y no importa si el reordenamiento en este bloque de código es.
La prohibición del reordenamiento volátil es el uso de barreras de memoria. La barrera de la memoria es agregar instrucciones relacionadas antes y después de la necesidad de prohibir el reordenamiento. Lo entiendo de esta manera: la CPU reordenará las instrucciones relacionadas debido a la optimización, o si hay una ejecución paralela en el método de canalización, agregar una barrera de memoria le dice al CPU que no se necesita ningún esfuerzo para optimizar, paso a paso en orden. Por lo tanto, la reordenación está prohibida, algunas optimizaciones no se utilizarán y el rendimiento debe degradarse.

Utilice escenarios de volátiles

Este artículo describe principalmente el uso de bloqueos de doble verificación Hay otros escenarios en los que se requiere volátil. Debido a la garantía de visibilidad de volatile, si la operación de escritura es demasiado frecuente, debe usarse con precaución, lo que provocará una degradación grave del rendimiento. Intente utilizar el escenario con más lecturas y menos escrituras.

Verifique dos veces el bloqueo antes de JDK1.5

La semántica de volátil para prohibir el reordenamiento solo está disponible en la 1.5 (incluida la 1.5) y versiones posteriores. En el antiguo JMM, volatile solo garantiza la visibilidad. Por lo tanto, en ese momento, parece que puede haber problemas con cómo escribir el bloqueo de doble verificación, y no es necesariamente seguro. Incluso vi el siguiente esquema, y ​​puede haber problemas debido a algunas razones sutiles.

public class Earth {
    
    
    private static volatile Earth earth = null;

    public static Earth getInstance() {
    
    
        if (earth == null) {
    
    
            Earth tmp = null;
            synchronized (Earth.class) {
    
    
                tmp = earth;
                if (tmp == null) {
    
    
                    synchronized (Earth.class) {
    
    
                        tmp = new Earth();
                    }
                    earth = tmp;
                }
            }
        }
        return earth;
    }
}

Usa singleton hambriento

Por supuesto, en un entorno multiproceso, no es necesario usar el bloqueo de doble verificación para crear un singleton. Se recomienda usar el modo singleton estático del hombre hambriento para crear una clase externa y usar el objeto singleton como su estático. campo.:

public class EarthHolder {
    
    

    private static final Earth EARTH = new Earth();

    public static Earth getInstance() {
    
    
        return EarthHolder.EARTH;
    }
}

Lectura recomendada

Puede leer los siguientes documentos. La captura de pantalla de la instrucción de prueba anterior es de aquí:
http://gee.cs.oswego.edu/dl/cpj/jmm.html
http: //www.cs.umd. Edu / ~ pugh / java / memoryModel / DoubleCheckedLocking.html

Supongo que te gusta

Origin blog.csdn.net/x763795151/article/details/109015712
Recomendado
Clasificación