Explicación detallada de la palabra clave volátil en la concurrencia de Java

¿Qué es la palabra clave volátil?

La palabra clave volátil se utiliza para modificar variables, las variables modificadas por esta palabra clave pueden garantizar la visibilidad y el orden.
Pero no puede alcanzar la atomicidad.
Puede considerarlo como una palabra clave sincronizada ligera y debilitada.
Usado para sincronización.

Comencemos con la descripción de las tres características mencionadas anteriormente.

Tres caracteristicas

La visibilidad, la atomicidad y el orden son la base de toda la concurrencia de Java.

  • Visibilidad: es decir, cuando un hilo modifica el valor de una variable compartida, después de esta operación, otros hilos leen la variable y leen los datos nuevos modificados en lugar de los datos antiguos.
  • Atomicidad: Una operación es indivisible y no se puede interrumpir, se ejecutará o no se ejecutará, es imposible decir que me detendré allí a la mitad de la ejecución.
  • Orden: por ejemplo, nuestro código se ejecuta en orden relativo. El código de la línea anterior se ejecuta primero y el código de la línea siguiente se ejecuta más tarde. ¿Por qué dices eso? En un entorno de un solo subproceso, de hecho puede verse como secuencial, pero no necesariamente desde una perspectiva de subprocesos múltiples. El compilador y la CPU reordenarán el código o las instrucciones bajo la premisa de garantizar el resultado correcto de un solo subproceso para la eficiencia de ejecución. Para un solo subproceso, no afecta, pero para varios subprocesos, esto es un problema.

Se puede decir que estas tres características son los problemas que esperamos resolver en la concurrencia de Java.
En respuesta a esto, JMM (Java Memory Model) se basa en estas tres características.

A continuación, presentaremos JMM

modelo de memoria java

En la parte inferior de nuestro hardware, la CPU necesita interactuar con la memoria principal (memoria).
Pero sabemos que los registros en la CPU son rápidos, pero la velocidad de la memoria principal es demasiado lenta en comparación con los registros.
Si la CPU interactúa directamente con la memoria, sería una pérdida de tiempo. La capacidad del registro es pequeña y el costo es demasiado alto.
Entonces, la capa inferior usa un caché para conectar el registro (cpu) con la memoria principal. Como la velocidad se encuentra entre los dos, el precio también es aceptable. Como caché para ambos.
Ahora, la capa inferior básica adopta este modelo. Pero diferentes CPU tienen diferentes modelos. Si las asigna directamente a los programadores, será demasiado problemático. Hay demasiados escenarios para que los programadores los consideren.
Así que Java crea su propio modelo de memoria para encapsular estos modelos y personaliza una lógica invariable predeterminada para proporcionar a los programadores. Este es el modelo de memoria de Java (JMM).

Es decir, entendemos la situación de la memoria subyacente, solo necesitamos considerar el modelo de memoria de Java, no necesitamos considerar qué tipo de CPU o desorden.
Inserte la descripción de la imagen aquí
El diagrama esquemático de JMM se muestra arriba.
El modelo aquí es un concepto lógico, no necesariamente real. Como programadores, no necesitamos considerar si realmente existe.

Cada subproceso tiene un subproceso de trabajo privado y el subproceso de trabajo está conectado a la memoria principal.
La memoria principal es compartida por todos los subprocesos, y lo que se almacena son variables compartidas (casi todas las variables de instancia, variables estáticas, objetos de clase, etc.)

  • Operación de lectura de subprocesos: primero vaciar las variables compartidas en la memoria principal a su propia memoria local, y luego leer desde la memoria local
  • Operación de escritura de subprocesos: primero escriba los datos en la memoria local y luego vacíe los datos en la memoria principal

Cabe señalar aquí que no importa si es lectura o escritura, no es atómico, son la operación de coincidencia de dos operaciones separadas.
Toda la lectura y escritura de datos del hilo debe realizarse en la memoria local y la memoria principal no se puede manipular directamente.

Problema de JMM

Este modelo de memoria se da cuenta de la caché, por lo que el rendimiento de la CPU es lo más grande posible y no hay necesidad de esperar a que la memoria sea lenta, tiene ventajas y, naturalmente, desventajas.
La separación de las operaciones de lectura y escritura provocará problemas de seguridad en los subprocesos.
Por ejemplo, el siguiente ejemplo:

	static int i =0;
	两个线程同时执行以下代码,结果会怎样呢?
	线程A执行将i=2;
	线程B读取i值;

El hilo A se ejecuta primero y el hilo B. ¿Se lee el hilo B 2?
No necesariamente, puede ser 0. Cuando el
subproceso A se ejecuta y coloca la variable i = 2 en la memoria local, el subproceso B vacía la i en la memoria principal a su propia memoria local. En este momento, la i en la memoria principal es 0, y luego la memoria local del subproceso A vacía i a la memoria principal.
Entonces, el resultado final es que i es 2 en la memoria principal, y el resultado leído por el hilo B es 0. Aunque según la lógica A se ejecuta primero y B se ejecuta después, el resultado debería ser 2. Pero este no es el caso, por lo que este es un problema causado por JMM. (El valor de i en el caché del subproceso AB es diferente aquí, por lo que el problema de la coherencia del caché está diseñado)

Reordenación de instrucciones

Además de los problemas anteriores, también se enfrenta al problema del reordenamiento de las instrucciones (código, que también puede entenderse como instrucciones de bytecode, que son casi iguales).

En primer lugar, ¿por qué está reordenando?
Para jvm y tiempo de compilación, el orden de código actual no es necesariamente más eficiente, por lo que para lograr la eficiencia, el orden debe interrumpirse.
Especialmente en un entorno concurrente, el reordenamiento se vuelve más importante.

Tomemos un ejemplo: las
CPU modernas suelen utilizar tecnología de canalización.
Debido a que es necesario ejecutar varias instrucciones, cada instrucción también se puede descomponer en diferentes pasos. Los registros (no como recurso) utilizados en cada paso son diferentes. Si solo se ejecuta una parte de una instrucción al mismo tiempo, excepto los recursos ocupa, otros recursos se desperdician.
Por lo tanto, utilizamos tecnología de canalización, como ejecutar la parte a de la instrucción 1 en el primer momento, ejecutar la parte b de la instrucción 2 al mismo tiempo y ejecutar la parte d de la instrucción 3 al mismo tiempo. Al mismo tiempo, se ejecutan varias instrucciones al mismo tiempo, lo que es mucho más eficiente.
Al mismo tiempo, si para una instrucción, si el orden de dos de los pasos se puede invertir, entonces no tenemos que esperar al paso 3 en el orden del paso 2 (esto se bloqueará), puedo elegir ejecutar paso 2 o paso primero según el mejor 3. De esta manera, reordenar las instrucciones lo hará más eficiente.

Al igual que en este ejemplo, nuestro reordenamiento de código es el mismo aquí, por eficiencia.

P.ej:

int i = 0;//1
int j = 1;//2
int a = i+j;//3

Por ejemplo, en el siguiente ejemplo, ¿tenemos que ejecutarlo en el orden de 123?
No necesariamente, si es más rápido, podemos usar el orden 213 y el resultado no cambiará en este momento.

Entonces puede que tenga que preguntar, ¿por qué no 312 aquí?
Muy simple, porque 3 depende de 12, se debe ejecutar 12 antes que 3, y el orden relativo de 12 no importa.
Podemos ver fácilmente, pero ¿cómo determinar la JVM?

Determinado por lo definido sucede antes de las reglas.

Sucede antes de las reglas

Esta es una regla predefinida y no se puede violar cuando el jvm está optimizado (reordenado).

  1. Principio del orden del programa: En un hilo, de acuerdo con la secuencia del código del programa, la operación escrita en el frente ocurre antes que la operación escrita en la parte posterior.
  2. Reglas volátiles: La escritura de variables volátiles ocurre antes de la lectura, lo que asegura la visibilidad de las variables volátiles.
  3. Reglas de bloqueo: el desbloqueo (desbloqueo) debe realizarse antes del bloqueo posterior (bloqueo).
  4. Transitividad: A precede a B y B precede a C, por lo que A debe preceder a C.
  5. El método de inicio del hilo precede a cada acción que realiza.
  6. Todas las operaciones del hilo preceden a la terminación del hilo.
  7. La interrupción del hilo (interrupt ()) precede al código interrumpido.
  8. El constructor del objeto termina antes del método finalize.

La primera regla La regla de orden del programa dice que en un hilo, todas las operaciones están en orden, pero en JMM, siempre que el resultado de la ejecución sea el mismo, se permite el reordenamiento. El énfasis de sucede-antes aquí también es único La corrección de los resultados de la ejecución del subproceso, pero no hay garantía de que lo mismo sea cierto para el subproceso múltiple. La segunda regla del monitor de reglas es realmente fácil de entender, es decir, antes de bloquear, asegúrese de que el bloqueo se haya liberado antes de poder continuar bloqueando. La tercera regla se aplica a los volátiles discutidos: si un hilo escribe una variable primero y otro hilo la lee, entonces la operación de escritura debe preceder a la operación de lectura. La cuarta regla es la transitividad de ocurre antes. Los siguientes artículos no se repetirán uno por uno.

En un solo subproceso, el reordenamiento no importa, porque el resultado real sigue siendo el mismo, pero con varios subprocesos, el problema es grande.

Por ejemplo, la siguiente pregunta:

int a = 0;
bool flag = false;

public void write() {
    
    
   a = 2;              //1
   flag = true;        //2
}

public void multiply() {
    
    
   if (flag) {
    
             //3
       int ret = a * a;//4
   }
}

El subproceso A ejecuta primero el método de escritura y el subproceso B ejecuta el método de multiplicación después.
¿Es el resultado 4, no necesariamente, si se lleva a cabo un reordenamiento, como se muestra a continuación?

	线程A		线程B
	2			
				3
				4
	1

Aquí 1, 2 es la regla de secuencia del programa, que se puede reordenar.
3 y 4 son interdependientes, por lo que 3 ocurre antes que 4, que no se puede reorganizar, y el problema se vuelve a excluir.
En el caso anterior, el resultado de ret es 0. No es el mismo que esperábamos.
Entonces hay un problema aquí, claramente esperamos que el resultado sea 4 en el código.

Sale la palabra clave volátil

Para solucionar el problema anterior, aparece el protagonista.

En primer lugar, para la primera pregunta:
establezca la variable estática i en volátil

	static volatile int i =0;
	两个线程同时执行以下代码,结果会怎样呢?
	线程A执行将i=2;
	线程B读取i值;

En este momento, las operaciones de lectura / escritura son todas atómicas, por lo que el hilo A escribe primero (está a punto de escribir en la memoria de trabajo, y los dos pasos para actualizar la memoria de trabajo a la memoria principal se combinan en uno y se actualizan inmediatamente después de escribir en la memoria de trabajo).
Luego se lee el hilo B, lo mismo es cierto si se obtiene, también se obtiene directamente después de refrescar.
El resultado final es 2.


Para la segunda pregunta:

int a = 0;
volatile bool flag = false;

public void write() {
    
    
   a = 2;              //1
   flag = true;        //2
}

public void multiply() {
    
    
   if (flag) {
    
             //3
       int ret = a * a;//4
   }
}

Aquí solo la bandera se define como volátil.
Mire la secuencia hb en este momento.
En el método de escritura, aquí hay una explicación: La
palabra clave volátil prohíbe el reordenamiento . La así llamada prohibición es que la operación de la variable ordinaria antes del código debe ocurrir antes que ella misma y no puede reorganizarse detrás de sí misma. De manera similar, esta última no puede ir parte delantera. La palabra clave volátil aquí es equivalente a una barrera, que separa las regiones superior e inferior de la suya. No es asunto mío reorganizar sus propias regiones, pero no puede perder el tiempo entre regiones. (De hecho, también se utilizan barreras de memoria. Por ejemplo, todas las operaciones de escritura antes de esta barrera se vacían en la memoria principal).
Por lo tanto, esto limita el orden de 1 a
2. Al mismo tiempo, debido a que se lee la escritura volátil hb, 2 -3
simultáneamente 3 y 4 Hay dependencias, entonces 3-4

Por lo tanto, finalmente se realizó el orden de 1-2-3-4, como deseamos.

Aquí está la realización de la visibilidad y el orden.


¿Qué pasa con la atomicidad?
¿No se dio cuenta el volátil que mencionamos anteriormente de la atomicidad de lectura / escritura, entonces por qué no es atómico?

La atomicidad de la que estamos hablando aquí es sobre i ++, que modifica un valor en función del valor original.
Esta no es una simple lectura o escritura.
La lógica es:

	先读
	修改
	写

Es una operación compuesta. Volátil no puede lograr la atomicidad de esta operación compuesta, por lo que no hay forma.

Imagine que para i = 0; el
hilo A ejecuta i ++; el hilo B también ejecuta i ++;
lo que podría suceder

	线程A		线程B
	读			
				读
	修改	
				修改
	写			
				写

De acuerdo con la secuencia anterior, hará que i no sea 2 sino 1. Cuando el hilo A obtiene i = 0, entonces el hilo B también lee 0. Cuando el hilo A ha terminado de escribir, el hilo B ya ha terminado de leer, por lo que es también 1 cuando está escrito. Entonces es diferente de lo que esperábamos.
Volatile no puede resolver este problema.
(Ps: aquí se puede considerar resolver a través de CAS, o bloquear)

para resumir

volátil logrado

  • Visibilidad: Los dos pasos de escribir en la memoria de trabajo y actualizar la memoria de trabajo en la memoria principal se combinan en uno. La memoria principal se actualiza en la memoria de trabajo y el valor obtenido por la CPU de la memoria de trabajo también se combina en uno, por lo que se hace la variable volátil. La lectura / escritura es atómica, por lo que se puede garantizar que sea visible.
    La operación para realizar la operación dos en uno es el prefijo de bloqueo en el ensamblaje, de modo que el caché de la cpu actual se vacía en la memoria y, al mismo tiempo, se invalidan otros cachés, por lo que otros cpus deben volver a adquirir el caché. (En otras palabras, actualice inmediatamente después de escribir y actualice y lea inmediatamente al leer)
  • Orden: La palabra clave volátil prohíbe el reordenamiento de instrucciones y se utilizan barreras de memoria. No importa cómo se reorganice su código antes y después de la barrera, pero no se puede seguir el frente y no se puede usar el último. Semánticamente, las escrituras antes de la barrera de la memoria deben vaciarse en la memoria, de modo que las lecturas posteriores a la barrera de la memoria puedan obtener los resultados de las escrituras anteriores. (Por lo tanto, la barrera de la memoria reducirá el nuevo rendimiento, lo que resultará en la incapacidad de optimizar el código)

No se ha implementado:

  • Atomicidad: solo se realiza la atomicidad de una sola operación. Por ejemplo, i ++ lee primero, modifica después y escribe en último lugar. Es una operación compuesta, por lo que la atomicidad no está garantizada.

Referencia

Programación concurrente Modelo de memoria Java + palabra clave volátil + HappenBefore regla
seguridad de subprocesos ( activada ) : comprender a fondo la palabra clave volátil Palabra clave
volátil favorita del entrevistador de Java

Supongo que te gusta

Origin blog.csdn.net/qq_34687559/article/details/114329619
Recomendado
Clasificación