Conocimiento profundo de la palabra clave volátil

Pensamientos de un problema de visibilidad

Veamos un fragmento de código:

public static boolean flg =false;
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread=new Thread(()->{
    
    
            int i=0;
            while (!flg){
    
    
                i++;
                //1. System.out.println("i:="+i);

                // 2.Thread.sleep(1000);
                /*try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
            }
        });
        thread.start();
        Thread.sleep(1000);
        flg=true;
    }

Resultado de la operación:
Inserte la descripción de la imagen aquí
si suelta el primer código de comentario o el segundo código de comentario, verá que el programa finaliza normalmente. Analicemos

imprimir hace que el ciclo termine

  • Debido a que la capa inferior de impresión usa la palabra clave sincronizada , indica que println tiene una operación de bloqueo, y la operación de liberar el bloqueo sincronizará forzosamente las operaciones de escritura involucradas en la memoria de trabajo con la memoria principal.

    public void println(String x) {
          
          
            synchronized (this) {
          
          
                print(x);
                newLine();
            }
        }
    
  • Desde el punto de vista de IO, la impresión es esencialmente una operación de IO, y la eficiencia de IO del disco debe ser mucho más lenta que la eficiencia de cálculo de la CPU, por lo que IO puede hacer que la CPU tenga tiempo de actualizar la memoria, lo que conduce a este fenómeno. Podemos verificar definiendo un nuevo Archivo ()

Thread.sleep (largo)

  • Thread.sleep (long) provocará el cambio de hilo, lo que provocará la invalidación de la caché y luego leerá el último valor

volátil

Sabemos que la principal razón por la que aparece el problema mencionado al principio del artículo es la visibilidad. Para garantizar la visibilidad de los hilos, Java propuso la palabra clave volatile.

¿Qué es visibilidad?

En un entorno multiproceso, después de que un subproceso actualiza una variable compartida, es posible que los subprocesos que acceden posteriormente a la variable no puedan leer el resultado actualizado de inmediato, o incluso nunca leer el resultado actualizado. Esta es otra manifestación de problemas de seguridad de subprocesos: visibilidad.

¿Por qué hay un problema de visibilidad?

1. Caché

La potencia de procesamiento de un procesador moderno (CPU) es mucho mejor que la velocidad de acceso de la memoria principal (DRAM). El tiempo que tarda la memoria principal en realizar una operación de lectura y escritura es suficiente para que el procesador ejecute cientos de instrucciones. Para cerrar la brecha entre el procesador y la memoria principal, los diseñadores de hardware introdujeron una caché entre la memoria principal y el procesador , como se muestra en la figura: La
Inserte la descripción de la imagen aquí
caché tiene una tasa de acceso mucho mayor que la de la memoria principal. Componentes de almacenamiento con una capacidad mucho menor que la memoria principal y cada procesador tiene su propia caché. Después de la introducción de la caché, el procesador no se ocupa directamente de la memoria principal cuando realiza operaciones de lectura y escritura, sino a través de la caché.
Los procesadores modernos generalmente tienen varios niveles de caché, como se muestra en la figura anterior. Hay caché de nivel 1 (caché L1), caché de nivel 2 (caché L2) y caché de nivel 3 (caché L3). Su orden de acceso: L1> L2> L3.
Cuando varios subprocesos acceden a la misma variable compartida, el caché del procesador de cada subproceso mantendrá una copia de la variable compartida. Esto ocasiona un problema: cuando un procesador actualiza los datos de copia , ¿Cómo saben y reaccionan otros procesadores de manera adecuada? Esto implica problemas de visibilidad. También conocido como problema de coherencia de caché

Problema de coherencia de caché (MESI)

El protocolo MESI (Modified-Exclusive-Shared-Invalid) es un protocolo de coherencia de caché ampliamente utilizado. El protocolo de coherencia utilizado por los procesadores x86 se basa en el protocolo MESI.
Para garantizar la coherencia de los datos, MESI divide el estado de entrada de la caché en cuatro tipos: Modificado, Exclusivo, Compartido, No válido y define un conjunto de mensajes (Mensaje) para coordinar las operaciones de lectura y escritura entre los distintos procesadores.

  • Inválido (inválido, marcado como I), significa que la línea de caché correspondiente no contiene ninguna copia válida correspondiente a la dirección de memoria. Este estado es el estado inicial de la entrada de caché.
  • Compartido (compartido, indicado como S), significa que la línea de caché correspondiente contiene los datos de copia correspondientes a la dirección de memoria correspondiente, y los cachés de otros procesadores también contienen los datos de copia correspondientes a la misma dirección de memoria, como se muestra en la figura:
    Inserte la descripción de la imagen aquí
  • Exclusivo (exclusivo, indicado como E), significa que la línea de caché correspondiente contiene los datos de copia correspondientes a la dirección de memoria correspondiente, y la caché de todos los demás procesadores no retiene los datos de copia, como se muestra en la figura:
    Inserte la descripción de la imagen aquí
  • Modificado (modificado, marcado como M), lo que significa que la línea de caché correspondiente contiene los datos de resultado actualizados de la dirección de memoria correspondiente. En el protocolo MESI, solo un procesador puede actualizar los datos correspondientes a la misma dirección de memoria en cualquier momento. FIG: El
    Inserte la descripción de la imagen aquí
    protocolo MESI define un conjunto de mensajes (Mensaje) para leer la coordinación de los distintos procesadores, operación de escritura en memoria, como:
    Inserte la descripción de la imagen aquí
    A continuación, echamos un vistazo breve a través del diagrama de flujo del proceso de trabajo Protocolo MESI:
    Inserte la descripción de la imagen aquí
    Desde arriba este Podemos ver una debilidad en el rendimiento del protocolo MESI: una vez que el procesador ha realizado la operación de memoria, debe esperar a que todos los demás procesadores almacenen en caché los datos de copia correspondientes en su caché y reciban la
    respuesta de confirmación / lectura de invalidación de estos procesadores. Solo después del mensaje se pueden escribir los datos en la caché.
    Para evitar y reducir el retraso de las operaciones de escritura causado por tal espera, los diseñadores de hardware han introducido cachés de escritura y colas de invalidación.
Escribir búfer (búfer de almacenamiento) e invalidar cola (invalidar cola)

El búfer de escritura (búfer de almacenamiento, también conocido como búfer de escritura) es un componente de caché privado dentro del procesador con una capacidad menor que la caché. Después de introducir el búfer de escritura, el procesador manejará la operación de la siguiente manera: si la entrada de caché correspondiente es S, entonces el procesador almacenará primero los datos relevantes de la operación de escritura (incluidos los datos y la dirección de memoria con la operación) en el búfer de escritura En la entrada, y enviar de forma asincrónica el mensaje Invalidate, es decir, el procesador de ejecución de la operación de escritura en memoria considerará que la operación de escritura se ha completado después de colocar los datos relevantes de la operación de escritura en el búfer de escritura, y no espera a que otros procesadores devuelvan Invalidate Acknowledge / Read El mensaje de respuesta continúa ejecutando otras instrucciones, lo que reduce el retraso de la operación de escritura.
Cola de invalidación. Después de recibir el mensaje de invalidación, el procesador no elimina los datos de copia correspondientes a la dirección de memoria especificada en el mensaje, sino que envía el mensaje. Después de almacenarse en la cola de invalidación, se devuelve el mensaje Invalidate Acknowledge, reduciendo así el tiempo de espera para el ejecutor de la operación de escritura.
El proceso es el siguiente:
Inserte la descripción de la imagen aquí
Sin embargo, el búfer de escritura y la cola de invalidación traerán algunos problemas nuevos: reordenación de instrucciones

2. Reordenamiento de instrucciones

Usamos un ejemplo para explicar en detalle el problema del reordenamiento de instrucciones :

int data =0;
boolean ready=false;
void threadDemo1(){
    
    
    data=1; //S1
    ready=true; //S2

}
void threadDemo2(){
    
    
   while (!ready){
    
     //S3
       System.out.println(data);  //S4
   }
}

Suponga que el caché de CPU0 tiene solo una copia de listo y el caché de CPU1 solo tiene una copia de datos. El
proceso de ejecución es el siguiente:
Inserte la descripción de la imagen aquí
Desde la perspectiva de CPU1, esto crea un fenómeno: S2 se ejecuta antes que S1

Barrera de la memoria

Qué tipo de reordenamiento de memoria es compatible con el procesador, y proporcionará instrucciones que pueden prohibir el reordenamiento correspondiente. Estas instrucciones se denominan barreras de memoria.
Las barreras de memoria se pueden representar mediante XY, donde las subtablas X e Y representan Carga (lectura) y Almacenamiento. (escribir). La función de la barrera de memoria es prohibir el reordenamiento entre cualquier operación X en el lado izquierdo de la instrucción y cualquier operación Y en el lado derecho de la instrucción, para garantizar que todas las operaciones X en el lado izquierdo de la instrucción se envíen antes de la operación Y en el lado derecho de la instrucción. Como se muestra abajo:
Inserte la descripción de la imagen aquí

principio

El principio de volatilidad se realiza realmente mediante el uso de la barrera de memoria subyacente. Podemos ver el pseudocódigo después de agregar la palabra clave volátil:

	volatile int data =0;
    boolean ready=false;
    void threadDemo1(){
    
    
        data=1;
        //StoreStore 确保前后的写操作已经写入到高速缓存中
        ready=true;

    }
    void threadDemo2(){
    
    
        while (!ready){
    
    
            //LoadLoad 确保ready的读操作在data的读操作之前
            System.out.println(data);
        }
    }

Para resumir: reglas de barrera de memoria de inserción de lectura / escritura volátiles:

  • Inserte la barrera LoadLoad y la barrera LoadStore después de cada operación de lectura volátil
  • Inserte una barrera StoreStore y una barrera StoreLoad antes y después de cada operación de escritura volátil

modelo sucede antes

El modelo de memoria Java (JMM) define el comportamiento de las palabras clave volátiles, finales y sincronizadas y asegura que los programas Java correctamente sincronizados puedan ejecutarse en procesadores de diferentes arquitecturas.
En términos de atomicidad, JMM estipula que las operaciones de lectura y escritura de tipos de datos básicos distintos de las variables largas / dobles y compartidas de tipos de referencia son todas atómicas. Además, JMM también estipula específicamente que las operaciones de lectura y escritura en variables compartidas largas / dobles modificadas volátiles también son atómicas.
Para problemas de visibilidad y orden, JMM usa el modelo de ocurre antes para responder a la
regla de ocurre antes de la siguiente manera:

  • Reglas de secuencia de programa : semántica aparentemente serial (como si fuera serial). El resultado de cualquier acción en un hilo es visible para otras acciones después de la acción en la secuencia del programa, y ​​estas acciones parecen ser ejecutadas y enviadas por el hilo mismo en la secuencia del programa.
  • Regla de bloqueo del monitor : la liberación del bloqueo del monitor ocurre antes de cada aplicación posterior del bloqueo.
    Nota: "liberación" y "aplicación" deben ser para el mismo tipo de instancia de bloqueo, es decir, la liberación de un bloqueo no tiene una relación de ocurrencia previa con la aplicación de otro bloqueo.
  • Reglas de variables volátiles : la operación de escritura de una variable volátil ocurre antes de cada operación de lectura subsiguiente para la variable.
    Nota: Debe ser para la misma variable volátil y, en segundo lugar, las operaciones de lectura y escritura para la misma variable volátil deben tener una relación de secuencia de tiempo.
  • Regla de inicio del hilo : llame al método start () del hilo antes de que ocurra cualquier acción en el hilo que se inicia.
  • Regla de terminación del hilo : cualquier acción en un hilo ocurre antes de cualquier acción realizada por el método de unión del hilo después de que el método de unión regresa
  • Regla de la transitividad : si A ocurre antes de B y B ocurre antes de C, entonces A ocurre antes de C.

para resumir

  • Volatile logra visibilidad a través de la coherencia de la caché
  • Volátil prohíbe el reordenamiento de instrucciones a través de la barrera de la memoria, asegurando así el orden
  • JMM utiliza el modelo sucede antes para describir de manera más concisa el problema de la visibilidad y el orden.

Supongo que te gusta

Origin blog.csdn.net/xzw12138/article/details/106403512
Recomendado
Clasificación