Comprensión profunda de JMM y Happens-Before

Hola a todos, soy Wang Youzhi , bienvenidos a charlar conmigo sobre tecnología y la vida en el extranjero.
Ven y únete a nuestro grupo de portadores de baldes de Java: personas de Java que se enriquecen juntas .

Recientemente estoy obsesionado con P5R, por lo que el progreso no es ideal, pero tengo que decir Gao Juan Xing YYDS. Sin más preámbulos, comencemos el tema de hoy, JMM y Happens-Before .

No hay muchas preguntas sobre ellos, básicamente solo dos:

  • ¿Qué son los JMM? Describa JMM en detalle.

  • Cuéntame sobre tu comprensión de JMM, ¿por qué está diseñado así?

Consejos : este artículo se centra en la teoría JMM.

¿Qué son los JMM?

JMM es el modelo de memoria de Java, el modelo de memoria de Java . La explicación del modelo de memoria en las preguntas frecuentes de JSR-133 es:

A nivel de procesador, un modelo de memoria define las condiciones necesarias y suficientes para saber que las escrituras en la memoria realizadas por otros procesadores son visibles para el procesador actual, y las escrituras realizadas por el procesador actual son visibles para otros procesadores.

A nivel de procesador, el modelo de memoria define las condiciones necesarias y suficientes para que los núcleos de procesador tengan visibilidad de las operaciones de escritura de memoria de los demás . así como también:

Además, las escrituras en la memoria se pueden mover antes en un programa; en este caso, otros subprocesos podrían ver una escritura antes de que realmente “ocurra” en el programa. Toda esta flexibilidad es por diseño: al brindar al compilador, el tiempo de ejecución o el hardware la flexibilidad para ejecutar operaciones en el orden óptimo, dentro de los límites del modelo de memoria, podemos lograr un mayor rendimiento.

Permite que el compilador, el tiempo de ejecución o el hardware ejecuten instrucciones en un orden óptimo para mejorar el rendimiento, dentro de los límites del modelo de memoria . El orden óptimo es el orden de ejecución de instrucciones obtenido por reordenación de instrucciones.

Resumamos el modelo de memoria a nivel de procesador:

  • Define la visibilidad de las operaciones de escritura entre núcleos ;

  • El reordenamiento de instrucciones está restringido .

Luego mira la descripción de JMM:

El modelo de memoria de Java describe qué comportamientos son legales en el código de subprocesos múltiples y cómo los subprocesos pueden interactuar a través de la memoria. Describe la relación entre las variables en un programa y los detalles de bajo nivel de almacenarlos y recuperarlos hacia y desde la memoria o registros en un real sistema informático. Hace esto de una manera que se puede implementar correctamente utilizando una amplia variedad de hardware y una amplia variedad de optimizaciones del compilador.

Extrae información clave de este pasaje:

  • JMM describe la legalidad del comportamiento en subprocesos múltiples y cómo los subprocesos interactúan a través de la memoria ;

  • Las diferencias de implementación entre el hardware y el compilador están protegidas para lograr efectos de acceso a la memoria consistentes .

Veamos el modelo de memoria, ¿qué es exactamente JMM?

  • Desde la perspectiva de JVM, JMM protege las diferencias subyacentes de diferentes hardware/plataformas para lograr efectos de acceso a la memoria consistentes ;

  • Desde la perspectiva de los desarrolladores de Java, JMM define la visibilidad de las operaciones de escritura entre subprocesos y restringe el reordenamiento de las instrucciones .

Entonces, ¿por qué tener un modelo de memoria?

Problemas de concurrencia "extraños"

Las 8 preguntas que debe saber sobre los subprocesos (arriba) dan los 3 elementos de la programación concurrente y los problemas causados ​​por la incapacidad de implementarlos correctamente A continuación, exploremos las razones subyacentes.

Sugerencias : agregue un poco de contenido relacionado con la programación de subprocesos en Linux.

La programación de subprocesos de Linux es una programación preventiva basada en intervalos de tiempo . Simplemente se entiende que el subproceso aún no ha terminado de ejecutarse, pero el intervalo de tiempo se ha agotado y el subproceso se suspende. Linux selecciona el subproceso con la prioridad más alta en la cola de espera para asignar intervalos de tiempo, por lo que la prioridad es alta. El hilo siempre se ejecutará .

Problemas de atomicidad causados ​​por el cambio de contexto

Tomemos count++como ejemplo la operación común de autoincremento. Intuitivamente, pensamos que la operación de autoincremento se realiza de una sola vez sin ninguna pausa. pero en realidad produce 3 instrucciones:

  • Instrucción 1: leerá counten el caché;

  • Instrucción 2: Ejecutar operación de autoincremento;

  • Instrucción 3: Escriba el valor autoincrementado counten la memoria.

Entonces aquí viene la pregunta, si dos subprocesos t1 y t2 realizan countoperaciones de autoincremento al mismo tiempo, y el cambio de subproceso ocurre después de que t1 ejecuta la instrucción 1, ¿qué sucederá en este momento?

inserte la descripción de la imagen aquí

Esperábamos que el resultado fuera 2, pero en realidad obtuvimos 1. Este es el problema de atomicidad causado por el cambio de subprocesos. Entonces, ¿prohibir el cambio de subprocesos no resuelve el problema de la atomicidad?

Aun así, el costo de prohibir el cambio de subprocesos es demasiado alto . Sabemos que la velocidad de computación de la CPU es "rápida", mientras que las operaciones de E/S son "lentas". Imagínese, si está usando Steam para descargar P5R, pero la computadora está atascada, solo puede escribir errores felizmente después de la descarga , ¿está enojado?

Por lo tanto, cuando un subproceso en el sistema operativo realiza una operación de E/S, entregará el segmento de tiempo de la CPU y se lo dará a otros subprocesos para mejorar la tasa de utilización de la CPU .

¡P5R es el mejor del mundo! ! !

Problemas de visibilidad causados ​​por el almacenamiento en caché

Puede pensar que en el ejemplo anterior, ¿no están los subprocesos t1 y t2 operando en el mismo subproceso count?

Parece ser el mismo count, pero en realidad es countuna copia en la memoria en un caché diferente. Porque, no solo hay una gran diferencia de velocidad entre E/S y CPU, sino que la diferencia entre memoria y CPU no es pequeña. Para compensar la diferencia, se agrega un caché de CPU entre memoria y CPU .

Cuando el núcleo de la CPU opera los datos de la memoria, primero copia los datos en el caché y luego opera la copia de datos en el caché, respectivamente.

inserte la descripción de la imagen aquí

Primero ignoremos el impacto de MESI, y podemos obtener que la modificación de las variables en el caché del subproceso no sea inmediatamente visible para otros subprocesos .

Consejos : El contenido básico del protocolo MESI se complementa en la expansión .

Problemas de pedido causados ​​por el reordenamiento de instrucciones

Además de las formas anteriores de mejorar la velocidad de carrera, existen otras "polillas": el reordenamiento de instrucciones . Cambiemos el ejemplo de las 8 preguntas que debe saber sobre los hilos (arriba) .

public static class Singleton {

  private Singleton instance;

  public Singleton getInstance() {
    if (instance == null) {
      synchronized(this) {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }

  private Singleton() {
  }
}

En Java, new Singleton()debe seguir 3 pasos:

  1. Asignar memoria;

  2. inicializar Singletonel objeto;

  3. apuntará instancea este recuerdo.

Analice las dependencias entre estos 3 pasos. La asignación de memoria se debe realizar primero, de lo contrario no se pueden realizar 2 y 3. En cuanto a 2 y 3, no importa quién se ejecute primero, no afectará la corrección de la semántica bajo un solo subproceso . no hay diferencia entre ellos.dependencia.

Pero cuando se trata de escenarios de subprocesos múltiples, la situación se complica:

inserte la descripción de la imagen aquí

En este momento, lo que obtiene el subproceso t2 instancees un objeto de instancia que no se ha inicializado y se produce el problema de orden causado por la reordenación .

Consejos : reordenación de instrucciones complementarias en la expansión .

¿Qué hizo JMM?

Antes de describir formalmente JMM, se mencionaron otros dos modelos de memoria en JSR-133:

  • Modelo de memoria de consistencia secuencial

  • Sucede antes del modelo de memoria

El modelo de memoria coherente secuencialmente prohíbe las optimizaciones del compilador y del procesador y ofrece sólidas garantías de visibilidad de la memoria . Requiere:

  • Durante la ejecución, todas las operaciones de lectura/escritura tienen una relación de orden total;

  • Las operaciones en el hilo deben ejecutarse en el orden del programa;

  • Las operaciones deben ejecutarse atómicamente y ser inmediatamente visibles para todos los subprocesos.

El modelo de consistencia secuencial es demasiado restrictivo y obviamente no es adecuado como modelo de memoria para lenguajes de programación que soportan concurrencia.

Sucede antes

Ocurre antes describe la relación entre los resultados de dos operaciones La operación A ocurre antes de la operación B (indicada como Ahb​B ) Incluso después de reordenar, el resultado de la operación A debe ser visible para la operación B.

Sugerencias : Sucede-Antes es una relación causal, Ahb​B es la "causa", el resultado de A es visible para B como el "efecto", y el proceso de ejecución no es de mi incumbencia.

La regla de Happens-Before, citamos la traducción en "El arte de la programación concurrente de Java":

Reglas de orden del programa : cada operación en un subproceso ocurre antes de cualquier operación posterior en ese subproceso. Supervise las reglas de bloqueo : el desbloqueo de un bloqueo ocurre antes, seguido del bloqueo de este bloqueo. Reglas de variables volátiles : la escritura de una variable volátil ocurre antes de cualquier lectura posterior de esta variable volátil. Transitividad : si A sucede antes que B y B sucede antes que C, entonces A sucede antes que C. regla start() : si el subproceso A ejecuta la operación ThreadB.start() (iniciar el subproceso B), entonces la operación ThreadB.start() del subproceso A ocurre antes de cualquier operación en el subproceso B. regla join() : si el subproceso A ejecuta la operación ThreadB.join() y regresa con éxito, entonces ocurre cualquier operación en el subproceso B, antes de que el subproceso A regrese con éxito de la operación ThreadB.join().

El contenido anterior aparece en JSR-133 Capítulo 5 Ocurre antes y se sincroniza con Edges , el texto original es difícil de leer.

Estos parecen una tontería, pero no olvidemos que estamos ante un entorno multiproceso , compilador y reordenación de hardware .

Nuevamente, tome la regla de bloqueo del monitor como ejemplo, aunque solo dice que el desbloqueo ocurre antes del bloqueo, el resultado real después del desbloqueo (éxito/fallo) ocurre antes del bloqueo.

Sugerencias : Sucede-Antes se puede traducir como sucediendo antes de… , Sincroniza-Con se puede traducir como sincronizando con… .

Además, JSR-133 también menciona las reglas para variables no volátiles :

Los valores que se pueden ver mediante una lectura no volátil están determinados por una regla conocida como "sucede antes de la coherencia".

Es decir, la visibilidad de las operaciones de lectura en variables no volátiles está determinada por la consistencia antes de que suceda .

Ocurre antes de la consistencia : hay una operación de escritura W y una operación de lectura R en la variable V. Si Whb​R se cumple, el resultado de la operación W es visible para la operación R (la definición en JSR 133 interpreta el rigor de los científicos) .

Aunque JMM no acepta todas las reglas Sucede antes (mejoradas), aún se puede considerar: Reglas Sucede antes ≈ Reglas JMM.

Entonces, ¿por qué elegir Happens-Before? De hecho, es el resultado de la compensación entre programación fácil , restricción y eficiencia operativa .

inserte la descripción de la imagen aquí

En la imagen, solo se seleccionan los modelos de memoria mencionados más o menos hoy, entre los cuales X86/ARM se refiere al sistema de arquitectura de hardware.

Aunque Happens-Before es el núcleo de JMM, además, JMM también protege las diferencias entre el hardware y proporciona a los desarrolladores de Java 3 primitivos de concurrencia, synchronized, volatileyfinal .

Expandir contenido

El contenido teórico sobre el modelo de memoria y JMM ha terminado, aquí tienes un complemento a los conceptos que aparecen en el artículo, la mayoría son a nivel de hardware, si no te interesa puedes saltártelo directamente.

Protocolo de coherencia de caché

Cache Coherence Protocol (Protocolo de coherencia de caché), la coherencia no es una coherencia común.

La coherencia y la consistencia a menudo aparecen en la programación concurrente, la optimización de la compilación y el diseño de sistemas distribuidos. Es fácil malinterpretarlo si solo lo entiende de la traducción al chino. De hecho, la diferencia entre los dos sigue siendo muy grande. Veamos el modelo de consistencia en Wikipedia explicación de:

La consistencia es diferente de la coherencia, que ocurre en los sistemas con o sin caché, y es la consistencia de los datos con respecto a todos los procesadores. La coherencia se ocupa de mantener un orden global en el que todos los procesadores ven las escrituras en una sola ubicación o una sola variable. La coherencia se ocupa de la ordenación de las operaciones en múltiples ubicaciones con respecto a todos los procesadores.

Obviamente, si es Coherencia, está dirigida a una sola variable, mientras que Consistencia está dirigida a múltiples conexiones.

acuerdo MESI

El protocolo MESI es el protocolo de coherencia de caché más utilizado basado en la invalidación. MESI representa 4 estados del caché:

  • M (Modificado, modificado) , los datos en el caché se han modificado y son diferentes de los datos en la memoria principal.

  • E (Exclusivo, exclusivo) , los datos solo existen en la memoria caché del núcleo actual y son los mismos que los datos de la memoria principal.

  • S (Compartido, compartido) , los datos existen en varios núcleos y son los mismos que los datos de la memoria principal.

  • I (No válido, no válido) , los datos en el caché no son válidos.

Sugerencias : además del protocolo MESI, existen el protocolo MSI, el protocolo MOSI, el protocolo MOESI, etc. Las iniciales describen el estado y O significa Propiedad.

MESI es una garantía hecha a nivel de hardware, que garantiza el orden de lectura y escritura de una variable en múltiples núcleos .

Las diferentes arquitecturas de CPU tienen diferentes implementaciones de MESI. Por ejemplo, X86 introduce el búfer de almacenamiento y ARM introduce el búfer de carga y la cola no válida. El búfer de lectura/escritura y la cola no válida mejoran la velocidad pero presentan otro problema.

reordenación de instrucciones

El reordenamiento se puede dividir en 3 categorías:

  • Reordenamiento paralelo de instrucciones : en ausencia de dependencias de datos, el procesador puede optimizar el orden de ejecución de las instrucciones por sí mismo;

  • Reordenación optimizada del compilador : el compilador puede reorganizar el orden de ejecución de las declaraciones sin cambiar la semántica de un solo subproceso;

  • Reordenación del sistema de memoria : introduce el búfer de almacenamiento/carga y lo ejecuta de forma asíncrona. Parece que las instrucciones se ejecutan "fuera de servicio".

Los dos primeros reordenamientos son fáciles de entender, pero ¿cómo entender el reordenamiento del sistema de memoria?

Introduzca el búfer de almacenamiento, el búfer de carga y la cola no válida, y modifique el proceso de interacción síncrona original a interacción asíncrona. Aunque se reduce el bloqueo síncrono, también trae la posibilidad de "fuera de servicio".

Por supuesto, reordenar no es "ningún tabú", tiene dos líneas fundamentales:

  • Dependencia de datos : dos operaciones dependen de los mismos datos, e incluye operaciones de escritura.En este momento, existe una dependencia de datos entre las dos operaciones. Si hay una dependencia de datos entre dos operaciones, el orden de las dos operaciones no se puede modificar cuando el compilador o procesador reordena ;

  • Semántica como si en serie : la semántica como si en serie no significa que se ejecuten como un escenario de un solo subproceso, pero no importa cómo se reordenen, la semántica en un escenario de un solo subproceso no se puede cambiar (o la ejecución el resultado permanece sin cambios) .

    Lectura recomendada

Lecturas en el modelo de memoria y el JMM

Aunque "Time, Clocks, and the Ordering of Events in a Distributed System" analiza problemas en el campo distribuido, también tiene un gran impacto en el campo de la programación concurrente.

Además, he preparado "Tiempo, relojes y ordenamiento de eventos en un sistema distribuido" y las versiones en chino e inglés de JSR-133, un total de 3 archivos PDF, solo responda a [JSR133].

Finalmente, una cosa interesante que decir, los blogs de los grandes son muy "simples".
Página principal del blog de Doug Lea:

inserte la descripción de la imagen aquí

Página de inicio del blog de Lamport:

inserte la descripción de la imagen aquí

epílogo

Recientemente soy adicto a P5R, he sido flojo~~

El contenido de JMM está muy enredado, porque cuando se trata del principio de concurrencia, nunca es el lenguaje de programación en sí mismo el que está luchando. Cada enlace de la CPU al lenguaje de programación está involucrado, por lo que es difícil controlar los detalles de cada parte del contenido.

Pero afortunadamente, también entiendo la esencia y la razón de JMM . Espero que este artículo te sea útil. Bienvenido a dejar un mensaje y corregirme.


Bueno, eso es todo por hoy, Adiós~~

Supongo que te gusta

Origin blog.csdn.net/wyz_1945/article/details/131689955
Recomendado
Clasificación