El modelo de memoria Java, denominado JMM, es una garantía unificada para una serie de plataformas de máquinas virtuales Java a la plataforma específica no relacionada para la visibilidad de la memoria y si se puede reordenar en un entorno múltiple proporcionado por los desarrolladores. (Puede haber ambiguo en términos del término y la distribución de memoria del tiempo de ejecución de Java, que se refiere a áreas de memoria como montón, área de método, pila de subprocesos, etc.).
Hay muchos estilos de programación concurrente. Además del CSP (proceso secuencial de comunicación), actor y otros modelos, el más familiar debería ser el modelo de memoria compartido basado en hilos y bloqueos. En la programación multiproceso, se deben prestar tres tipos de problemas de concurrencia:
・ Atomicidad ・ Visibilidad ・ Reorden
La atomicidad implica si otros hilos pueden ver el estado intermedio o interferir cuando un hilo realiza una operación compuesta. Por lo general, es el problema de I ++. Dos hilos realizan operaciones ++ en la memoria de montón compartido al mismo tiempo. La implementación de operaciones ++ en JVM, tiempo de ejecución y CPU puede ser una operación compuesta. Por ejemplo, desde la perspectiva de las instrucciones JVM, es leer el valor de I desde la memoria de almacenamiento en la pila de operando, agregar una y escribir nuevamente a la memoria de montón i. Durante estas operaciones, si no hay una sincronización correcta, otros hilos también pueden ejecutarlo al mismo tiempo, lo que puede conducir a la pérdida de datos y otros problemas. Los problemas de atomicidad comunes, también conocidos como condición competitiva, se juzgan en función de un posible resultado de falla, como la lectura-modificación-escritura. Los problemas de visibilidad y reordenamiento se derivan de la optimización del sistema.
Dado que la velocidad de ejecución de la CPU y la velocidad de acceso de la memoria están gravemente no coincidentes, para optimizar el rendimiento, en función de los principios de localización, como la localidad del tiempo y la localidad espacial, la CPU ha agregado un caché de múltiples capas entre la memoria. Cuando es necesario obtener datos, la CPU primero irá a la memoria caché para averiguar si existe el caché correspondiente. Si existe, se devolverá directamente. Si no existe, se obtendrá en la memoria y se guardará en el caché. Ahora, cuanto más procesadores de múltiples núcleos se han vuelto estándar, cada procesador tiene su propio caché, lo que implica el problema de la consistencia del caché. Las CPU tienen modelos de consistencia de diferentes fortalezas y debilidades. La consistencia más fuerte es la mayor seguridad, y también se ajusta a nuestro modo de pensamiento secuencial. Sin embargo, en términos de rendimiento, habrá muchos gastos generales debido a la necesidad de una comunicación coordinada entre diferentes CPU.
Un diagrama típico de estructura de caché de la caché de la CPU es el siguiente
El ciclo de instrucciones de la CPU suele ser la obtención de instrucciones, analizar las instrucciones para leer datos, ejecutar instrucciones y volver a escribir datos a registros o memoria. Al ejecutar instrucciones en Serial, los datos de lectura y almacenado llevan mucho tiempo, por lo que la CPU generalmente usa la tubería de instrucciones para ejecutar múltiples instrucciones al mismo tiempo para mejorar el rendimiento general, al igual que una tubería de fábrica.
La velocidad de leer datos y escribir datos a la memoria no está en el mismo orden de magnitud que la ejecución de instrucciones, por lo que la CPU usa registros y cachés como cachés y búferes. Al leer datos de la memoria, leerá una línea de caché (similar a la lectura del disco y leerá un bloque). El módulo que devuelve los datos volverá a colocar la solicitud de almacenamiento en un búfer de almacenamiento cuando los datos antiguos no estén en el caché y continúa ejecutando la siguiente etapa del ciclo de instrucción. Si existe en el caché, el caché se actualizará y los datos en la memoria caché se enjuagarán a la memoria de acuerdo con una determinada política.
public class MemoryModel {private int count; parada booleana privada; public void initCountAndStop () {count = 1; parar = falso; } public void doloop () {while (! stop) {count ++; }} public void Printresult () {System.out.println (Count); System.out.println (parar); }}Al ejecutar el código anterior, podemos pensar que Count = 1 se ejecutará antes de parar = falso. Esto es correcto en el estado ideal que se muestra en el diagrama de ejecución de la CPU anterior, pero es incorrecto al considerar el registro y el almacenamiento en caché. Por ejemplo, Stop en sí está en el caché, pero el recuento no está allí, entonces la parada se puede actualizar y el búfer de escritura de recuento se actualiza a la memoria antes de volver a escribir.
Además, la CPU y el compilador (generalmente se refieren a JIT para Java) pueden modificar la orden de ejecución de instrucciones. Por ejemplo, en el código anterior, Count = 1 y Stop = false no tienen dependencias, por lo que la CPU y el compilador pueden modificar el orden de estos dos. A la vista de un programa único, el resultado es el mismo. Esta es también el AS-IF-Serial que la CPU y el compilador deben garantizar (independientemente de cómo se modifique el orden de ejecución, el resultado de ejecución del subproceso único permanece sin cambios). Dado que la mayor parte de la ejecución del programa es de un solo hilo, dicha optimización es aceptable y trae grandes mejoras de rendimiento. Sin embargo, en el caso de la lectura múltiple, pueden ocurrir resultados inesperados sin las operaciones de sincronización necesarias. Por ejemplo, después de que el hilo T1 ejecuta el método initCountandStop, el hilo T2 ejecuta Printresult, que puede ser 0, falso, 1, falso o 0, verdadero. Si Thread T1 ejecuta Doloop () primero y Thread T2 se ejecuta initCountAndStop un segundo, entonces T1 puede saltar del bucle, o es posible que nunca vea la modificación de la parada debido a la optimización del compilador.
Debido a los diversos problemas en las situaciones de subproceso múltiple anterior, la secuencia del programa en múltiples subprocesos ya no es el orden de ejecución y da como resultado el mecanismo subyacente. El lenguaje de programación debe garantizar a los desarrolladores. En términos simples, esta garantía es cuando la modificación de un hilo será visible para otros hilos. Por lo tanto, el lenguaje Java propone JavamemoryModel, es decir, el modelo de memoria Java, que requiere la implementación de acuerdo con las convenciones de este modelo. Java proporciona mecanismos como volátiles, sincronizados y finales para ayudar a los desarrolladores a garantizar la corrección de los programas multiprocesos en todas las plataformas de procesadores.
Antes de JDK1.5, el modelo de memoria de Java tenía serios problemas. Por ejemplo, en el modelo de memoria anterior, un hilo puede ver el valor predeterminado de un campo final después de que se complete el constructor, y la escritura del campo volátil puede reordenarse con la lectura y la escritura del campo no volátil.
Entonces, en JDK1.5, se propuso un nuevo modelo de memoria a través de JSR133 para solucionar los problemas anteriores.
Reordenar las reglas
bloqueo volátil y monitor
| ¿Es posible reordenar | La segunda operación | La segunda operación | La segunda operación |
|---|---|---|---|
| La primera operación | Lectura normal/escritura ordinaria | Lectura/monitor volátil | Salida de escritura/monitor volátil |
| Lectura normal/escritura ordinaria | No | ||
| Lectura/monitor de voaltile | No | No | No |
| Salida de escritura/monitor volátil | No | No |
La lectura normal se refiere a la matriz de matriz de Getfield, Getstatic y no volátiles, y la lectura normal se refiere a la tienda de matriz de Putfield, Putstatic y no volátiles.
La lectura y la escritura de campos volátiles son Getfield, Getstatic, Putfield, Putstatic, respectivamente.
Monitorenter debe ingresar al bloque de sincronización o el método de sincronización, el monitorexista se refiere a salir del bloque de sincronización o el método de sincronización.
No en la tabla anterior se refiere a dos operaciones que no permiten el reordenamiento. Por ejemplo (escritura normal, escritura volátil) se refiere al reordenamiento de campos no volátiles y al reordenamiento de escrituras de cualquier campo volátil posterior. Cuando no hay no, significa que se permite reordenar, pero el JVM necesita garantizar una seguridad mínima: el valor de lectura es el valor predeterminado o escrito por otros subprocesos (las operaciones de lectura y escritura dobles y largas de 64 bits son un caso especial. Cuando no hay una modificación volátil, no se garantiza que la lectura y la escritura son atómicas, y la capa subyacente puede dividir en dos operaciones separadas).
Campo final
Hay dos reglas especiales adicionales para el campo final
Ni la escritura del campo final (en el constructor) ni la escritura de la referencia del objeto de campo final en sí se pueden reordenar con las escrituras posteriores de los objetos que sostienen el campo final (fuera del constructor). Por ejemplo, la siguiente declaración no se puede reordenar
X.Finalfield = V; ...; SharedRef = x;
La primera carga del campo final no se puede reordenar con la escritura del objeto que contiene el campo final. Por ejemplo, la siguiente declaración no permite reordenar.
x = SharedRef; ...; i = x.finalfield
Barrera de memoria
Todos los procesadores admiten ciertas barreras o cercas de memoria para controlar la visibilidad de reordenar y datos entre diferentes procesadores. Por ejemplo, cuando la CPU vuelve a escribir los datos, colocará la solicitud de la tienda en el búfer de escritura y esperará a la memoria. Esta solicitud de la tienda se puede evitar que se reordene con otras solicitudes insertando la barrera para garantizar la visibilidad de los datos. Puede usar un ejemplo de vida para comparar la barrera. Por ejemplo, al tomar un elevador de pendiente en el metro, todos ingresan al ascensor en secuencia, pero algunas personas irán desde la izquierda, por lo que el orden al salir del ascensor es diferente. Si una persona lleva un gran equipaje bloqueado (barrera), las personas detrás no pueden dar la vuelta :). Además, la barrera aquí y la barrera de escritura utilizada en GC son conceptos diferentes.
Clasificación de barreras de memoria
Casi todos los procesadores respaldan las instrucciones de barrera de un cierto grano grueso, generalmente llamado cerca (cerca, valla), lo que puede garantizar que las instrucciones de carga y almacenamiento iniciadas antes de la cerca puedan estar estrictamente en orden con la carga y almacenar después de la cerca. Por lo general, se dividirá en los siguientes cuatro tipos de barreras de acuerdo con su propósito.
Barrera de carga
Carga1; Carga de carga; Carga2;
Asegúrese de que los datos de Load1 se carguen antes de load2 y después de la carga
Barreras de Storestore
Tienda1; Storestore; Tienda2
Asegúrese de que los datos en Store1 sean visibles para otros procesadores antes de Store2 y después.
Barreras de carga
Carga1; Tienda de carga; Tienda2
Asegúrese de que los datos de Load1 se carguen antes de Store2 y después de la descarga de datos
Barreras
Tienda1; Storeload; Carga2
Asegúrese de que los datos en Store1 sean visibles frente a otros procesadores (como el enjuague a la memoria) antes de cargar los datos en Load2 y después de la carga. La barrera de Storeload evita que la carga lea los datos antiguos en lugar de los datos recientemente escritos por otros procesadores.
Casi todos los multiprocesadores en los tiempos modernos requieren una gran cantidad. La sobrecarga de Storeload suele ser la más grande, y Storeload tiene el efecto de otras tres barreras, por lo que Storeload puede usarse como una barrera general (pero más alta).
Por lo tanto, utilizando la barrera de memoria anterior, se pueden implementar las reglas de reordenamiento en la tabla anterior
| Necesito barreras | La segunda operación | La segunda operación | La segunda operación | La segunda operación |
|---|---|---|---|---|
| La primera operación | Lectura normal | Escritura normal | Lectura/monitor volátil | Salida de escritura/monitor volátil |
| Lectura normal | Tienda de carga | |||
| Lectura normal | Almacén | |||
| Lectura/monitor de voaltile | Carga | Tienda de carga | Carga | Tienda de carga |
| Salida de escritura/monitor volátil | Seto | Almacén |
Para apoyar las reglas de los campos finales, es necesario agregar una barrera a la escritura final a
X.Finalfield = V; Storestore; SharedRef = x;
Insertar barrera de memoria
Según las reglas anteriores, puede agregar una barrera al procesamiento de campos volátiles y palabras clave sincronizadas para cumplir con las reglas del modelo de memoria.
Inserte el storestore antes de la barrera de la tienda volátil después de que se escriban todos los campos finales, pero inserte el storestore antes de que el constructor regrese
Inserte la barrera de Storeload después de la tienda volátil. Inserte la carga de carga y la barrera de la tienda de carga después de la carga volátil.
El monitor ingrese y las reglas de carga volátiles son consistentes, y la salida del monitor y las reglas de la tienda volátiles son consistentes.
Suceder
Las diversas barreras de memoria mencionadas anteriormente siguen siendo relativamente complejas para los desarrolladores, por lo que JMM puede usar una serie de reglas de relaciones de orden parcial de sucesión antes de ilustrar. Para garantizar que el hilo que ejecute la Operación B ve el resultado de la Operación A (independientemente de si A y B se ejecutan en el mismo hilo), entonces la relación sucesiva debe cumplirse entre A y B, de lo contrario, el JVM puede reordenarlos arbitrariamente.
HACE BORE REGLAS LISTA
Feliz antes de las reglas incluyen
Reglas de secuencia del programa: si la operación A en el programa está antes de la operación B, entonces la operación A en el mismo subproceso realizará reglas de bloqueo del monitor antes de la operación B: la operación de bloqueo en el bloqueo del monitor debe realizarse antes de la operación de bloqueo en el mismo bloqueo del monitor.
Reglas de variables volátiles: la operación de escritura de la variable volátil debe ejecutar reglas de inicio de subproceso antes de la operación de lectura de la variable: la llamada al subproceso. La operación B se ejecuta antes de la operación C, luego la operación A se ejecuta antes de la operación C.
El bloqueo de visualización tiene la misma semántica de memoria que el bloqueo del monitor, y la variable atómica tiene la misma semántica de memoria que la volátil. La adquisición y el lanzamiento de los bloqueos, las operaciones de lectura y escritura de variables volátiles satisfacen la relación de orden completo, por lo que la escritura de volátil se puede realizar antes de las lecturas volátiles posteriores.
El hambre mencionado anteriormente se puede combinar utilizando múltiples reglas.
Por ejemplo, después de que el subproceso A ingresa al bloqueo del monitor, la operación antes de liberar el bloqueo del monitor se basa en las reglas de secuencia del programa, y la operación de liberación del monitor se utiliza para obtener el mismo bloqueo de monitor en el subproceso posterior B, y la operación en la operación en Ocvent y subproceso B. B.
Resumir
Lo anterior es toda la explicación detallada del modelo de memoria Java JMM en este artículo, espero que sea útil para todos. Si hay alguna deficiencia, deje un mensaje para señalarlo. ¡Gracias amigos por su apoyo para este sitio!