La palabra clave volátil en detalle

1. Caso de error

       La palabra clave volátil se introduce a través de un caso, como el siguiente ejemplo de código: En este momento , la comunicación entre los dos subprocesos sin la palabra clave volátil será problemática.

public class ThreadsShare {
    
    
  private static boolean runFlag = false; // 此处没有加 volatile
  public static void main(String[] args) throws InterruptedException {
    
    
    new Thread(() -> {
    
    
      System.out.println("线程一等待执行");
      while (!runFlag) {
    
    
      }
      System.out.println("线程一开始执行");
    }).start();
    Thread.sleep(1000);
    new Thread(() -> {
    
    
      System.out.println("线程二开始执行");
      runFlag = true;
      System.out.println("线程二执行完毕");
    }).start();
  }
}

Resultado de salida:
imagen

Conclusión: El subproceso 1 no sintió la señal de que el subproceso 2 ha cambiado runFlag a verdadero, por lo que la oración "El comienzo de la ejecución del subproceso" no se ha emitido y el programa no ha finalizado.

Al igual que el siguiente escenario:
Inserte la descripción de la imagen aquí

       En el escenario actual, puede parecer que el procesador A y el procesador B no volcaron los datos en sus respectivos búferes de escritura a la memoria y asignaron los valores A = 0 y B = 0 leídos de la memoria a X e Y En este momento, los datos del búfer se vacían en la memoria, lo que hace que el resultado final sea inconsistente con el resultado deseado real. Debido a que solo los datos en el búfer se vacían en la memoria es la ejecución real

La causa de este problema :

       Cuando una computadora ejecuta un programa, cada instrucción se ejecuta en el procesador. En el proceso de ejecución de instrucciones, la lectura y escritura de datos seguramente estarán involucradas. Los datos temporales durante la ejecución del programa se almacenan en la memoria principal (memoria física). En este momento, hay un problema. Debido a que el procesador se ejecuta rápido, el proceso y el procesamiento de la lectura de datos de la memoria y la escritura de datos en la memoria La velocidad en el que el procesador ejecuta instrucciones es mucho más lento que eso, por lo que si cualquier operación en los datos se realiza a través de la interacción con la memoria, reducirá en gran medida la velocidad de ejecución de la instrucción. Para resolver este problema, se diseña la caché de la CPU. Cuando cada subproceso ejecuta una declaración, primero leerá el valor de la memoria principal, luego lo copiará a la memoria local y luego realizará operaciones de datos para actualizar el último valor a Memoria principal. Esto provocará un fenómeno de caché inconsistente

En respuesta al fenómeno anterior, se propone un protocolo de coherencia de caché: MESI

       La idea central es: El protocolo MESI garantiza que las copias de las variables compartidas utilizadas en cada caché sean coherentes. Cuando el procesador escribe datos, si se encuentra que la variable operativa es una variable compartida, es decir, existe una copia de la variable en otros procesadores, enviará una señal a otros procesadores para invalidar la línea de caché de la variable compartida ( bus sniffing) Mecanismo de exploración ), por lo que cuando otros procesadores necesitan leer esta variable y encuentran que la línea de caché que almacena la variable en caché no es válida, entonces la volverá a leer de la memoria.

Protocolo de coherencia de caché de rastreo:

       Todas las transferencias de memoria ocurren en un bus de memoria compartida y todos los procesadores pueden ver este bus. El caché en sí es independiente, pero la memoria es compartida. Todos los accesos a la memoria deben ser arbitrados, es decir, solo un procesador puede leer y escribir datos en el mismo ciclo de instrucción. El procesador no solo interactúa con el bus de memoria durante las transferencias de memoria, sino que también intercambia datos constantemente en el bus de rastreo para rastrear lo que están haciendo otros cachés, por lo que cuando un procesador lee y escribe memoria, se notificará a otros procesadores (notificación proactiva). esto para sincronizar sus guardados en caché. Siempre que un procesador escriba en la memoria, otros procesadores sabrán que esta memoria no es válida en su segmento de caché.

MESI detallado:

En el protocolo MESI, cada línea de caché tiene cuatro estados:

  1. Modificado significa que esta línea de datos es válida, los datos se han modificado y los datos en la memoria son inconsistentes, y los datos solo existen en la caché actual
  2. Exclusivo de Exclusivo , esta fila de datos es válida, los datos son consistentes con los datos en la memoria y los datos solo existen en esta caché
  3. Compartido compartido, esta fila de datos es válida, los datos son consistentes con los datos en la memoria y los datos se almacenan en muchas cachés.
  4. No válido esta fila de datos no es válida

       Aquí no válido, compartido y modificado están en línea con el protocolo de coherencia de caché de rastreo, pero exclusivo significa exclusivo, los datos actuales son válidos y consistentes con los datos en la memoria, pero solo el estado exclusivo en el caché actual resuelve que un procesador es lectura Antes de escribir la memoria tenemos que avisar a otros procesadores de este problema, el procesador solo puede escribir cuando la línea de caché es Exclusiva y Modificada, es decir, solo en estos dos estados el procesador monopoliza la línea de caché.

       Cuando un procesador quiere escribir una línea de caché, si no tiene derechos de control, primero debe enviar una solicitud de derechos de control al bus. En este momento, se notificará a otros procesadores para invalidar sus copias del mismo segmento de caché. El procesador puede modificar los datos cuando tiene el control, y en este momento, el procesador solo tiene una copia de la línea de caché y solo en su caché, no habrá conflicto, de lo contrario, si otros procesadores siempre quieren leer la línea de caché , La línea de caché exclusiva o modificada debe devolverse primero al estado compartido ; si es una línea de caché modificada, el contenido debe volver a escribirse primero en la memoria

       Así que Java proporciona un mecanismo de sincronización ligero volátil

2. Función

       Volátil es un mecanismo de sincronización ligero proporcionado por Java. Volátil es liviano porque no causa el cambio de contexto de hilo ni la programación. Pero la sincronización de variables volátiles es deficiente, no puede garantizar la sincronización de un bloque de código y su uso es más propenso a errores. La palabra clave volátil se utiliza para garantizar la visibilidad, es decir, para garantizar la visibilidad de la memoria de las variables compartidas para resolver el problema de coherencia de la caché . Una vez que una variable compartida es modificada por la palabra clave volátil, tiene dos semánticas: visibilidad de memoria y prohibición de reordenamiento de instrucciones. En un entorno multiproceso, la palabra clave volátil se usa principalmente para percibir la modificación de variables compartidas de manera oportuna y permitir que otros subprocesos obtengan inmediatamente el último valor de la variable.

El efecto del programa después de usar la palabra clave volátil:

Cómo utilizar:

private volatile static boolean runFlag = false;

Código:

public class ThreadsShare {
    
    
  private volatile static boolean runFlag = false;
  public static void main(String[] args) throws InterruptedException {
    
    
    new Thread(() -> {
    
    
      System.out.println("线程一等待执行");
      while (!runFlag) {
    
    
      }
      System.out.println("线程一开始执行");
    }).start();
    Thread.sleep(1000);
    new Thread(() -> {
    
    
      System.out.println("线程二开始执行");
      runFlag = true;
      System.out.println("线程二执行完毕");
    }).start();
  }
}

Resultado de salida:
imagen

Conclusión: El hilo 1 detecta la señal de que el hilo 2 ha cambiado runFlag a verdadero, por lo que se emite la oración "comienza la ejecución del hilo" y el programa finaliza.

Dos efectos de volátiles :

  1. Cuando un hilo escribe una variable volátil, JMM actualizará a la fuerza el valor de la variable en la memoria local correspondiente al hilo en la memoria principal
  2. Esta operación de escritura hará que el caché de esta variable compartida en otros subprocesos deje de ser válido. Si desea utilizar esta variable, debe volver a tomar el valor en la memoria principal.

Pensando: ¿Qué pasa si dos procesadores leen o modifican la misma variable compartida al mismo tiempo?

Para acceder a la memoria, varios procesadores primero deben obtener el bloqueo del bus de memoria. Solo un procesador puede obtener el control del bus de memoria en cualquier momento, por lo que la situación anterior no ocurrirá.

Importante: la palabra clave volátil se utiliza para garantizar la visibilidad, es decir, para garantizar la visibilidad de la memoria de las variables compartidas para resolver el problema de coherencia de la caché


3. Características

3.1 Visibilidad

Cuando una variable compartida se modifica de forma volátil , se asegurará de que el valor modificado se actualice inmediatamente a la memoria principal. Cuando otros hilos necesiten leerlo, irá a la memoria para leer el nuevo valor. No se puede garantizar la visibilidad de las variables compartidas ordinarias, porque después de que se modifican las variables compartidas ordinarias, es incierto cuándo se escribirán en la memoria principal. Cuando se leen otros hilos, la memoria puede tener todavía el valor anterior original en este momento, por lo que No se puede garantizar la visibilidad (el caso anterior ya ha demostrado el papel de la visibilidad )

3.2 Prohibición de reorganización de pedidos

       En el modelo de memoria de Java, el compilador y el procesador pueden reordenar instrucciones, pero el proceso de reordenación no afectará la ejecución de programas de un solo subproceso, pero afectará la corrección de la ejecución concurrente de múltiples subprocesos.

La palabra clave volátil prohíbe el reordenamiento de instrucciones tiene dos significados:

  1. Cuando el programa ejecuta la operación de lectura o escritura de la variable volátil, se deben haber realizado todos los cambios en las operaciones anteriores, y los resultados han sido visibles para las operaciones posteriores, las operaciones posteriores no deben haberse realizado;
  2. Al optimizar las instrucciones, no puede colocar las instrucciones que acceden a la variable volátil detrás de ella para su ejecución, y no puede colocar las instrucciones que siguen a la variable volátil antes de esta para su ejecución.

       Para resolver el error de memoria causado por el reordenamiento del procesador, el compilador de Java inserta instrucciones de barrera de memoria en la posición apropiada de la secuencia de instrucciones generada para prohibir tipos específicos de reordenamiento del procesador.

Instrucción de barrera de memoria: la barrera de memoria es la realización de semántica volátil, que se explicará a continuación

Tipo de barrera Ejemplo de comando Descripción
LoadLoadBarriers Carga1; Carga Carga; Carga2 La carga de datos Load1 ocurre antes que Load2 y todas las cargas de datos posteriores
TiendaStoreBarriers Tienda1; TiendaStore; Tienda2 El vaciado de datos de Store1 al almacenamiento principal ocurre antes de Store2 y todos los datos posteriores al almacenamiento principal
LoadStoreBarriers Load1; LoadStore; Store2 La carga de datos Load1 ocurre antes de Store2 y todos los datos posteriores se vacían de nuevo a la memoria principal
StoreLoadBarriers Store1; StoreLoad; Load2 La descarga de datos de Store1 en la memoria ocurre antes de Load2 y todas las cargas de datos posteriores

4.volátil 与 sucede antes

public class Example {
    
    
  int r = 0;
  double π = 3.14;
  volatile boolean flag = false; // volatile 修饰
  /**
   * 数据初始化
   */
  void dataInit() {
    
    
    r = 1; // 1
    flag = true; // 2
  }
  /**
   * 数据计算
   */
  void compute() {
    
    
    if(flag){
    
     // 3
      System.out.println(π * r * r); //4
    }
  }
}

       Si el subproceso A ejecuta dataInit (), el subproceso B ejecuta compute () de acuerdo con las reglas proporcionadas por pasa antes ( se cubre el modelo de memoria Java anterior ) El modelo de memoria Java tiene una charla sobre el paso 2 debe estar en línea con la regla volátil antes del paso 3, el paso 1 está en Antes del paso 2, el paso 3 está antes del paso 4, por lo que el paso 1 también está antes del paso 4 según las reglas transitivas.


5. Semántica de la memoria

5.1 Leer semántica de la memoria

       Al leer una variable volátil, la memoria de trabajo local se vuelve inválida y el valor actual de la variable modificada volátil se obtiene de la memoria.

5.2 Escribir la semántica de la memoria

       Al escribir una variable volátil, el valor en la memoria de trabajo local se fuerza de regreso a la memoria.

5.3 Implementación de la semántica de la memoria

Tabla de reglas de reordenamiento volátil de JMM formulada por el compilador

Puede reordenar Segunda operación
Primera operación Lectura o escritura ordinaria lectura volátil escritura volátil
Ordinario o escrito NO
lectura volátil NO NO NO
escritura volátil NO NO

Por ejemplo, el significado de la última celda de la tercera fila:

       Cuando una operación local es una operación normal, si la segunda operación es una escritura volátil, el compilador no puede reordenar estas dos operaciones.

5.4 Resumen

  1. Cuando la segunda operación es una escritura volátil, la primera operación no se puede reordenar pase lo que pase. Esta regla garantiza que las operaciones anteriores a la escritura volátil no pueden ser reorganizadas por el compilador detrás de la escritura volátil.
  2. Cuando la primera operación es de lectura volátil, no importa cuál sea la segunda operación, no se puede reordenar. Esta regla asegura que las operaciones posteriores a la lectura volátil no serán compiladas por el compilador antes de la lectura volátil.
  3. Cuando la primera operación es de escritura volátil y la segunda operación es de lectura volátil, no se puede reordenar

       Para realizar la semántica de memoria de volátil, el compilador inserta una barrera de memoria en la secuencia de instrucciones para prohibir ciertos tipos de clasificación del procesador al generar código de bytes.

Estrategia de inserción de barrera de memoria JMM:

  1. Inserte una barrera StoreStore antes de cada operación de escritura volátil.
  2. Inserte una barrera StoreLoad después de cada operación de escritura volátil.
  3. Inserte una barrera LoadLoad después de cada operación de lectura volátil.
  4. Inserte una barrera LoadStore después de cada operación de lectura volátil.

Diagrama esquemático de la secuencia de instrucciones generada después de que se inserta la escritura volátil en la barrera de memoria:

Inserte la descripción de la imagen aquí

       La barrera StoreStore puede garantizar que todas las operaciones de escritura ordinarias anteriores sean visibles para cualquier procesador antes de la escritura volátil. Esto se debe a que la barrera StoreStore garantizará que todas las escrituras ordinarias anteriores se vacíen en la memoria principal antes de la escritura volátil.

       La barrera StoreLoad puede garantizar el reordenamiento de escrituras volátiles y posibles operaciones subsecuentes de lectura o escritura volátiles.

Diagrama esquemático de la secuencia de instrucciones generada después de la lectura volátil insertada en la barrera de memoria:

imagen

       La barrera LoadLoad se usa para prohibir que el procesador reordene la lectura volátil arriba y la lectura normal abajo.

       La barrera LoadStore se utiliza para prohibir que el procesador reordene la lectura volátil anterior y la lectura y escritura normal a continuación.


6. Combate real

6.1 El uso de volátiles debe cumplir las condiciones

  • Las operaciones de escritura en variables no dependen del valor actual
  • La variable no se incluye en una invariante con otras variables.

       De hecho, estas condiciones indican que los valores efectivos que se pueden escribir en variables volátiles son independientes del estado de cualquier programa, incluido el estado actual de la variable. De hecho, las dos condiciones anteriores son para asegurar que la operación de la variable volátil sea atómica , para asegurar que el programa que usa la palabra clave volátil se pueda ejecutar correctamente cuando sea concurrente.

6.2 Escenarios en los que se utiliza principalmente volátil

       Perciba la modificación de las variables compartidas a tiempo en un entorno de subprocesos múltiples y permita que otros subprocesos obtengan inmediatamente el último valor de la variable

Escenario 1: Cantidad de marcas de estado (ejemplo en el texto)

public class ThreadsShare {
    
    
  private volatile static boolean runFlag = false; // 状态标记
  public static void main(String[] args) throws InterruptedException {
    
    
    new Thread(() -> {
    
    
      System.out.println("线程一等待执行");
      while (!runFlag) {
    
    
      }
      System.out.println("线程一开始执行");
    }).start();
    Thread.sleep(1000);
    new Thread(() -> {
    
    
      System.out.println("线程二开始执行");
      runFlag = true;
      System.out.println("线程二执行完毕");
    }).start();
  }
}

Escena dos doble verificación

La versión DCL del modo singleton es la abreviatura de bloqueo de doble verificación, y el nombre chino es mecanismo de recuperación de dos extremos. La llamada búsqueda de dos extremos consiste en emitir un juicio antes y después de bloquear

public class Singleton1 {
    
    
    private static Singleton1 singleton1 = null;
    private Singleton1 (){
    
    
        System.out.println("构造方法被执行.....");
    }
    public static Singleton1 getInstance(){
    
    
        if (singleton1 == null){
    
     // 第一次check
            synchronized (Singleton1.class){
    
    
                if (singleton1 == null) // 第二次check
                    singleton1 = new Singleton1();
            }
        }
        return singleton1 ;
    }
 }

       Utilice sincronizado para bloquear solo la parte del código que crea la instancia, no todo el método. Tanto antes como después de que se juzgue el bloqueo, esto se denomina mecanismo de recuperación de dos extremos. Esto realmente solo crea un objeto. Sin embargo, esto no es absolutamente seguro. Un nuevo objeto también se divide en tres pasos:

  • 1. Asignar espacio de memoria de objetos
  • 2. Inicializa el objeto
  • 3. Apunte el objeto a la dirección de memoria asignada, en este momento el objeto no es nulo

       No hay dependencia de datos en los pasos dos y tres, por lo que el compilador permite que estas dos oraciones inviertan el orden durante la optimización. Cuando se reorganizan las instrucciones, habrá problemas con el acceso de subprocesos múltiples. Así que existe la siguiente versión final del patrón singleton. En este caso, no se producirá ningún cambio de orden

public class Singleton2 {
    
    
  private static volatile Singleton2 singleton2 = null;
  private Singleton2() {
    
    
    System.out.println("构造方法被执行......");
  }
  public static Singleton2 getInstance() {
    
    
    if (singleton2 == null) {
    
     // 第一次check
      synchronized (Singleton2.class) {
    
    
        if (singleton2 == null) // 第二次check
          singleton2 = new Singleton2();
      }
    }
    return singleton2;
  }
}

Supongo que te gusta

Origin blog.csdn.net/weixin_38071259/article/details/112345166
Recomendado
Clasificación