Comprensión profunda de jvm y Happens-Before

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 sólo dos:

  • ¿Qué son los JMM? Describe JMM en detalle.
  • Cuénteme sobre su 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 Java, modelo de memoria 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 que las escrituras realizadas por el procesador actual son visibles para otros procesadores.

A nivel del procesador, el modelo de memoria define las condiciones necesarias y suficientes para que los núcleos del procesador tengan visibilidad de las operaciones de escritura en la memoria de cada uno . 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 darle 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 las instrucciones obtenido mediante el reordenamiento de las 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 Java describe qué comportamientos son legales en código multiproceso y cómo los subprocesos pueden interactuar a través de la memoria. Describe la relación entre las variables de un programa y los detalles de bajo nivel de su almacenamiento y recuperación hacia y desde la memoria o registros en un entorno real. sistema informático. Lo hace de una manera que se puede implementar correctamente utilizando una amplia variedad de hardware y una amplia variedad de optimizaciones del compilador.

Extraiga 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 consistentes de acceso a la memoria .

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 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 debes 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.

Consejos : agregue un pequeño contenido relacionado con la programación de hilos 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 agota y el subproceso está suspendido. Linux selecciona el subproceso con la mayor prioridad 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 como ejemplo la operación común de incremento automático count++. 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 el recuento en el caché;
  • Instrucción 2: Ejecute la operación de autoincremento;
  • Instrucción 3: Escriba el conteo autoincrementado en la memoria.

Entonces, aquí surge la pregunta: si dos subprocesos t1 y t2 realizan una operación de incremento automático en el conteo al mismo tiempo, y se produce un cambio de subproceso después de que t1 ejecuta la instrucción 1, ¿qué sucederá en este momento?

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 cálculo 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, cederá el intervalo 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, los subprocesos t1 y t2 no operan con el mismo recuento?

Parece el mismo recuento, pero en realidad es una copia del recuento en la memoria en diferentes cachés. Porque no solo existe 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 datos de la memoria, primero copia los datos en el caché y luego opera la copia de datos en el caché respectivamente.

Primero ignoremos el impacto de MESI y podremos lograr que la modificación de las variables en el caché por parte 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 aumentar la velocidad de carrera, existen otras "polillas": el reordenamiento de instrucciones . Cambiemos el ejemplo de las 8 preguntas que debes 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() {
	}
}

New Singleton() en Java debe seguir 3 pasos:

  1. Asignar memoria;
  2. Inicialice el objeto Singleton;
  3. Apunte la instancia a esta memoria.

Analice las dependencias entre estos 3 pasos. La asignación de memoria debe realizarse 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 en un solo subproceso . No hay diferencia entre ellos.dependencia.

Pero cuando se trata de escenarios multiproceso, la situación se complica:

En este momento, la instancia obtenida por el hilo t2 es un objeto de instancia que no se ha inicializado y ocurre el problema de orden causado por el reordenamiento .

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 secuencialmente consistente prohíbe optimizaciones del compilador y del procesador y proporciona 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 de forma atómica y ser inmediatamente visibles para todos los subprocesos.

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

Sucede antes

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

Consejos : Sucede antes es una relación causal,  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 Sucede antes, citamos la traducción en "El arte de la programación concurrente de Java":

Reglas de orden del programa : cada operación en un hilo ocurre antes de cualquier operación posterior en ese hilo.

Supervisar 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 de B y B sucede antes de C, entonces A sucede antes de 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 cualquier operación en el subproceso B ocurre 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 Sucede antes y se sincroniza con los bordes , el texto original es difícil de leer.

Esto parece una tontería, pero no olvidemos que nos enfrentamos a un entorno multiproceso , compilador y reordenamiento 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.

Consejos : Happens-Before se puede traducir como sucediendo antes… , Synchronizes-With se puede traducir como sincronizar 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 coherencia sucede antes.

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

Sucede antes de la coherencia : hay una operación de escritura W y una operación de lectura R en la variable V. Si se cumple WhbR, 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 Sucede antes? De hecho, es el resultado del equilibrio entre programación sencilla , restricciones y eficiencia operativa .

En la imagen, sólo 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, JMM también protege las diferencias entre hardware y proporciona a los desarrolladores de Java 3 primitivas de concurrencia: sincronizada, volátil y final .

Ampliar contenido

Se acabó el contenido teórico sobre el modelo de memoria y JMM, aquí tienes un complemento a los conceptos que aparecen en el artículo, la mayoría de ellos son a nivel de hardware, si no te interesa te lo puedes saltar directamente.

Protocolo de coherencia de caché

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

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

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

Evidentemente, si se trata de Coherencia, se dirige a una única variable, mientras que la Consistencia se dirige a múltiples conexiones.

Acuerdo MESI

El protocolo MESI es el protocolo de coherencia de caché basado en invalidación más utilizado. 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 el 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.

Consejos : 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 realizada 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 traen otro problema.

reordenamiento 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;
  • Reordenamiento optimizado del compilador : el compilador puede reorganizar el orden de ejecución de las declaraciones sin cambiar la semántica de un solo subproceso;
  • Reordenamiento del sistema de memoria : introduzca el búfer de almacenamiento/carga y ejecútelo de forma asincrónica. Parece que las instrucciones se ejecutan "desordenadas".

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 sincrónica original a interacción asincrónica. Aunque el bloqueo sincrónico se reduce, también brinda la posibilidad de "fuera de servicio".

Por supuesto, reordenar no "no es un tabú", tiene dos conclusiones fundamentales:

  • Dependencia de datos : Dos operaciones dependen de los mismos datos e incluyen operaciones de escritura . En este momento, existe una dependencia de datos entre las dos operaciones. Si existe una dependencia de datos entre dos operaciones, el orden de las dos operaciones no se puede modificar cuando el compilador o el procesador reordena ;
  • semántica como si fuera en serie : la semántica como si fuera 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 sobre el modelo de memoria y el JMM.

Aunque "Tiempo, relojes y ordenación de eventos en un sistema distribuido" analiza cuestiones en el campo distribuido, también tiene un gran impacto en el campo de la programación concurrente.

Además, he preparado "Hora, relojes y ordenación 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, algo interesante que decir, los blogs de los grandes son muy "simples". Página de inicio del blog de Doug Lea:

Página de inicio del blog de Lamport:

epílogo

Recientemente soy adicto al P5R, he sido un vago ~~

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

Supongo que te gusta

Origin blog.csdn.net/m0_74433188/article/details/132531756
Recomendado
Clasificación