Comprensión profunda del modelo de memoria jvm-java (combinado con volátil)

1. estructura

Inserte la descripción de la imagen aquí

  • Todas las variables se almacenan en la memoria principal.
  • Cada hilo tiene su propia memoria de trabajo
  • La memoria de trabajo de un hilo contiene una copia de la memoria principal de las variables utilizadas por el hilo
  • Todas las operaciones en las variables por subprocesos deben realizarse en la memoria de trabajo y no deben manipular directamente la memoria principal
  • La transferencia de variables entre subprocesos debe realizarse de forma interactiva por la memoria principal

Inserte la descripción de la imagen aquí

2. Operaciones atómicas interactivas entre memoria

Lock (lock): una variable que actúa en la memoria principal, identifica una variable como un estado exclusivo de subproceso.

Desbloqueo (desbloqueo): una variable que actúa en la memoria principal, libera una variable que está en estado bloqueado, y la variable liberada puede ser bloqueada por otros subprocesos.

· Lectura (lectura): una variable que actúa en la memoria principal, transfiere el valor de una variable desde la memoria principal a la memoria de trabajo del hilo para acciones de carga posteriores.

· Carga (carga): actúa sobre las variables de la memoria de trabajo, coloca el valor de la variable obtenida de la memoria principal por la operación de lectura en la copia variable de la memoria de trabajo.

Uso (uso): una variable que actúa en la memoria de trabajo. Transmite el valor de una variable en la memoria de trabajo al motor de ejecución. Esta operación se realizará cada vez que la máquina virtual encuentre una instrucción de código de bytes que necesite usar el valor de la variable.

Asignar (asignación): una variable que actúa en la memoria de trabajo. Asigna un valor recibido del motor de ejecución a una variable de memoria de trabajo. Esta operación se realiza cada vez que la máquina virtual encuentra una instrucción de código de bytes que asigna un valor a la variable.

· Almacenar (almacenamiento): una variable que actúa en la memoria de trabajo, transfiere el valor de una variable en la memoria de trabajo a la memoria principal para su uso en operaciones de escritura posteriores.

· Escribir (escribir): actúa sobre las variables de la memoria principal, coloca los valores de las variables obtenidas de la memoria de trabajo por la operación de almacenamiento en las variables de la memoria principal.

Si desea copiar una variable de la memoria principal a la memoria de trabajo, debe realizar las operaciones de lectura y carga en orden. Si desea sincronizar las variables de la memoria de trabajo a la memoria principal, debe realizar las operaciones de almacenamiento y escritura en orden. Tenga en cuenta que el modelo de memoria Java solo requiere que las dos operaciones anteriores se realicen en secuencia , pero no requiere una ejecución continua.

3. comprensión volátil

  • Garantice la visibilidad de esta variable para todos los subprocesos (una vez que un subproceso cambia el valor, el nuevo valor puede ser conocido inmediatamente por otros subprocesos) (la seguridad del subproceso no está garantizada, por ejemplo: ABA 2 1 3, etc.)
  • Prohibir el reordenamiento de instrucciones

Las variables modificadas por volátil tienen las siguientes propiedades:

❑ Visibilidad. Al leer una variable volátil, siempre puede ver (cualquier hilo) la última escritura en la variable volátil.

❑ Atomicidad: la lectura / escritura de cualquier variable volátil es atómica , pero la operación compuesta similar a ++ volátil no es atómica. (Por ejemplo, largo, doble)

Reordenamiento (también comprende leer después de escribir, escribir después de leer, escribir después de escribir)

Qué es el reordenamiento: para mejorar el rendimiento, los compiladores y procesadores a menudo reordenan las instrucciones en un orden de ejecución de código determinado.
Motivo: Un buen modelo de memoria realmente relajará las restricciones sobre las reglas del procesador y del compilador, lo que significa que tanto la tecnología de software como la de hardware luchan por el mismo objetivo
: sin cambiar los resultados de la ejecución del programa, tanto como sea posible. Mejora la eficiencia de ejecución. JMM minimiza las restricciones en la capa inferior para que pueda ejercer sus propias
ventajas. Por lo tanto, para mejorar el rendimiento al ejecutar programas, los compiladores y procesadores a menudo reordenan las instrucciones. El reordenamiento general se puede dividir en los siguientes tres
tipos:
1. Reordenamiento optimizado del compilador. El compilador puede reorganizar el orden de ejecución de las declaraciones sin cambiar la semántica del programa de subproceso único:
2. Reordenación paralela a nivel de instrucción. Los procesadores modernos usan tecnología paralela de nivel de instrucción para superponer múltiples instrucciones. Si no hay dependencia de datos, el procesador
puede cambiar el orden de ejecución de las instrucciones de la máquina correspondientes a la declaración:
3. Reordenamiento del sistema de memoria. Debido a que el procesador usa caché y memorias intermedias de lectura / escritura, esto hace que las operaciones de carga y almacenamiento parezcan ejecutarse fuera de orden.
Inserte la descripción de la imagen aquí

Inserte la descripción de la imagen aquí
Si no utiliza la modificación volátil al definir la variable inicializada, el último código "initialized = true" en el hilo A puede ejecutarse de antemano debido a la optimización del reordenamiento de instrucciones (aunque Java se utiliza como pseudocódigo, pero el pesado La optimización de clasificación es una operación de optimización a nivel de máquina, y la ejecución temprana significa que el código de ensamblaje correspondiente a esta declaración se ejecuta por adelantado), de modo que el código que usa la información de configuración en el hilo B puede tener errores, y la palabra clave volátil puede evitar tales situaciones. Suceder

Hay otro ejemplo:
Inserte la descripción de la imagen aquí

Puede haber i = 0, j = 0

Motivo:
aquí el subproceso uno y el segundo pueden escribir variables compartidas en su propio búfer de escritura (A1, B1) al mismo tiempo, luego leer otra variable compartida (A2, B2) de la memoria y finalmente escribirse en el búfer Los datos sucios guardados se actualizan en la memoria (A3, B3). Cuando se ejecuta en esta secuencia de tiempo, el programa puede obtener el resultado de x = y = 0.
Inserte la descripción de la imagen aquí


El consumo de rendimiento de las operaciones de lectura de variables volátiles es casi el mismo que el de las variables ordinarias, pero las operaciones de escritura pueden ser más lentas porque requiere insertar muchas instrucciones de barrera de memoria en el código local para garantizar que el procesador no se ejecute fuera de orden.

4. Para tipos básicos de 64 bits largos y dobles

Si varios subprocesos comparten una variable de tipo largo o doble que no se declara volátil, y los lee y modifica al mismo tiempo, entonces algunos subprocesos pueden leer una variable que no es el valor original ni otro El valor modificado representa el valor de "media variable". Pero este tipo de lectura de "media variable" es muy raro

5. Sucede antes

Es un método muy útil para juzgar si los datos compiten y si el hilo es seguro.

Una operación "ocurre primero en el tiempo" no significa que la operación "ocurra primero". Entonces, si una operación "ocurre primero", ¿se puede deducir que esta operación debe ser "tiempo primero"? Desafortunadamente, esta inferencia también es insostenible.

Hay una relación de antes de pasar entre las dos operaciones, lo que no significa que la operación anterior deba ejecutarse antes de la siguiente. sucede antes solo requiere que la operación anterior (el resultado de la ejecución) sea visible para la última operación , y la operación anterior se clasifica antes de la segunda operación (la primera es visible y ordenada antes de la segunda).

Básicamente no existe una relación causal entre la secuencia de tiempo y el principio de ocurrencia previa, por lo tanto, cuando medimos problemas de seguridad concurrentes, la secuencia de tiempo no debe molestarnos. Todo debe basarse en el principio de ocurrencia previa.

i = 1;       //线程A执行
j = i ;      //线程B执行

¿Es j igual a 1? Suponga que la operación del hilo A (i = 1) ocurre antes de la operación del hilo B (j = i).
Entonces se puede determinar que j = 1 debe establecerse después de la ejecución del hilo B.
Si no tienen el principio de sucede antes, entonces j = 1 puede no ser cierto.

(Incluso si el código ejecuta j = 1 primero, luego ejecuta j = i, no es necesariamente j = 1, depende principalmente de si se cumple antes)


Aquí hay algunas relaciones preventivas "naturales" bajo el modelo de memoria Java

Regla de orden de programa: en un subproceso, de acuerdo con el orden de flujo de control, la operación escrita en el frente ocurre antes de la operación escrita en la parte posterior. Tenga en cuenta que el orden del flujo de control no es el orden del código del programa, porque se debe considerar la estructura de ramas y bucles.

· Regla de bloqueo del monitor: se produce una operación de desbloqueo antes de la operación de bloqueo del mismo bloqueo . Lo que debe enfatizarse aquí es "la misma cerradura", y "detrás" se refiere al orden cronológico.

· Regla de variable volátil: una operación de escritura en una variable volátil ocurre primero en una operación de lectura posterior en esta variable . Aquí "atrás" también se refiere al orden cronológico.

Regla de inicio de subproceso (regla de inicio de subproceso): el método start () del objeto de subproceso se produce primero en cada acción de este subproceso.

· Regla de terminación de subprocesos (regla de terminación de subprocesos): todas las operaciones en el subproceso ocurren primero en la detección de terminación de este subproceso, podemos pasar el método Thread :: join () al final, el valor de retorno Thread :: isAlive () y otros medios Compruebe si el hilo ha finalizado la ejecución.

Regla de interrupción de subproceso (regla de interrupción de subproceso): la llamada al método de interrupción de subproceso () se produce antes de que el código del subproceso interrumpido detecte la ocurrencia de un evento de interrupción. Puede detectar si se produce una interrupción a través del método Thread :: interrupted ().

Regla de finalización de objeto (regla de finalizador): la inicialización de un objeto (la finalización de la ejecución del constructor) se produce primero al comienzo de su método finalize ().

· Transitividad: si la operación A ocurre antes de la operación B y la operación B ocurre antes de la operación C, entonces se puede concluir

Basado en volátiles pasa antes

Inserte la descripción de la imagen aquí
1) De acuerdo con las reglas de la secuencia del programa, 1 sucede antes de 2; 3 pasa antes de 4.

2) De acuerdo con la regla volátil, 2 sucede antes que 3.

3) De acuerdo con la regla transitiva de pasa antes, 1 pasa antes 4.

6.Cambio de estado de hilo Java

Inserte la descripción de la imagen aquí
· Bloqueado: el subproceso está bloqueado. La diferencia entre "estado bloqueado" y "estado de espera" es que el "estado de bloqueo" está esperando que se adquiera un bloqueo exclusivo. Este evento se producirá cuando otro subproceso abandone el bloqueo. ; El "estado de espera" está esperando un período de tiempo o se produce una acción de activación. Cuando el programa está esperando ingresar al área de sincronización, el hilo ingresará a este estado.
Inserte la descripción de la imagen aquí

7. Semántica de memoria basada en volátiles.

  • Escribir semántica: al escribir una variable volátil, JMM actualizará el valor de la variable compartida en la memoria local correspondiente al hilo a la memoria principal.
  • Leer semántica: al leer una variable volátil, JMM invalidará la memoria local correspondiente al hilo. El hilo leerá la variable compartida de la memoria principal.

Implementación de semántica de memoria volátil (implementación de prohibición de reordenamiento de instrucciones)

❑ Inserte una barrera StoreStore frente a cada operación de escritura volátil.
❑ Inserte una barrera StoreLoad después de cada operación de escritura volátil.
❑ Inserte una barrera LoadLoad después de cada operación de lectura volátil.
❑ Inserte una barrera LoadStore después de cada operación de lectura volátil.

Inserte la descripción de la imagen aquí
Ejemplos:
Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

Publicado 37 artículos originales · elogiado 6 · visitas 4637

Supongo que te gusta

Origin blog.csdn.net/littlewhitevg/article/details/105568194
Recomendado
Clasificación