Red de conocimiento informático - Aprendizaje de programación - Introducción al JMM del modelo de memoria Java

Introducción al JMM del modelo de memoria Java

1) JSR133:

La especificación del lenguaje Java señala que JMM es un intento relativamente pionero de definir un modelo de memoria multiplataforma consistente, pero tiene algunas deficiencias menores pero importantes. . De hecho, las palabras clave más confusas en el lenguaje Java son principalmente sincronizadas y volátiles. Debido a esto, los desarrolladores a menudo ignoran estas reglas durante el proceso de desarrollo, lo que también dificulta la escritura de código sincrónico.

El propósito de JSR133 en sí es corregir algunos defectos del JMM original. Sus propios objetivos son los siguientes: Conservar la garantía de seguridad de la JVM de destino para realizar comprobaciones de seguridad de tipo: Proporcionar (out-of-thin). -seguridad aérea) para crear seguridad de la nada, por lo que "sincronizado correctamente" debe definirse formal e intuitivamente. Los programadores deben tener confianza en el desarrollo de programas multiproceso. Por supuesto, no hay otra forma de hacer que los programas concurrentes sean fáciles de realizar. desarrollar, pero el objetivo principal del lanzamiento de la especificación es aliviar a los programadores de la carga de comprender algunos de los detalles del modelo de memoria y proporcionar implementaciones JVM de alto rendimiento en una amplia gama de arquitecturas de hardware populares que varían en los procesadores actuales. en gran medida en sus modelos de memoria. Debería ser adecuado para tantas arquitecturas como sea posible sin sacrificar el rendimiento. Esta es también la base del diseño multiplataforma de Java. Proporcionar un lenguaje de sincronización para permitir la publicación de un objeto para hacerlo visible sin sincronización. En este caso, la nueva garantía de seguridad, también conocida como seguridad de inicialización, debería tener un impacto mínimo en el código existente. 2) La sincronización y la asincronía solo se refieren a la comprensión conceptual y no involucran algunas operaciones de la base informática subyacente:

. Durante el proceso de desarrollo del sistema, a menudo nos encontramos con estos conceptos básicos, ya sea comunicación de red, comunicación de mensajes entre objetos o solicitudes Http comúnmente utilizadas por los desarrolladores web, encontraremos estos conceptos, que a menudo se mencionan como un método de comunicación asincrónico. Entonces, ¿qué es exactamente esta descripción conceptual?

Sincronización: Sincronización significa que cuando se emite una llamada a una función, la llamada no volverá hasta que se reciba una respuesta. De hecho, la mayoría de las ejecuciones de programas se llaman de forma sincrónica. al describir operaciones sincrónicas y asincrónicas, se refiere principalmente al procesamiento de algunas tareas que requieren procesamiento colaborativo por parte de otros componentes o requieren una respuesta colaborativa. Por ejemplo, hay un hilo A. Durante la ejecución de A, es posible que B deba proporcionar algunos datos de ejecución relevantes. Por supuesto, lo que desencadena la respuesta de B es que A envía una solicitud a B o realiza una operación de llamada a B. Si A. está realizando la operación Cuando es sincrónico, A permanecerá en esta posición y esperará a que B envíe un mensaje de respuesta. Cuando B no recibe ningún mensaje de respuesta, A no puede hacer otras cosas y solo puede esperar. Operación de A. Es una explicación simple de la sincronización.

Asincrónico: Asincrónico significa que al emitir una llamada de función, no hay necesidad de esperar una respuesta y continúa haciendo lo que debe hacer. Una vez que se recibe la respuesta, se procesará en un. Hasta cierto punto, pero no afectará el proceso de procesamiento normal. Por ejemplo, hay un hilo A. Durante la ejecución de A, B también necesita proporcionar algunos datos u operaciones relevantes. Cuando A envía una solicitud a B o realiza una operación de llamada en B, A no necesita continuar esperando. pero ejecuta lo que A debe hacer una vez que B tenga una respuesta, notificará a A. Cuando A reciba la respuesta a la solicitud asincrónica, realizará el procesamiento relevante. En este caso, la operación de A es una operación asincrónica simple.

3) Visibilidad y ordenabilidad

Dos conceptos clave del modelo de memoria Java: visibilidad (Visibility) y ordenabilidad (Ordering)

Programadores que desarrollan multiproceso Los programas saben que la palabra clave sincronizada impone un mutex (exclusión mutua) entre subprocesos que evita que varios subprocesos entren en la sincronización protegida por un determinado monitor a la vez, es decir, en este caso, alguna memoria única para el ejecutante. El código del programa está en modo exclusivo y otros subprocesos no pueden acceder a la memoria exclusiva de su proceso de ejecución. Esta situación se denomina memoria no visible. Pero en el modo de sincronización de este modelo, hay otro aspecto: JMM señala que la JVM puede proporcionar algunas reglas de visibilidad de la memoria al manejar esta aplicación. En esta regla, garantiza que cuando haya un bloque sincronizado, el caché se actualice y. invalidado cuando se ingresa un bloque de sincronización. Por lo tanto, dentro del bloque sincronizado protegido por un monitor determinado dentro de la JVM, el valor escrito por un subproceso es visible para todos los demás subprocesos que ejecutan el bloque sincronizado protegido por el mismo monitor. Esta es una descripción de sexo visible simple. Esta máquina garantiza que el compilador no mueva instrucciones desde el interior de un bloque sincronizado hacia el exterior, aunque a veces sí mueve instrucciones desde el exterior hacia el interior. JMM no ofrece dicha garantía de forma predeterminada: se debe utilizar la sincronización siempre que varios subprocesos accedan a la misma variable. Breve resumen:

La visibilidad es un modo de compartir memoria durante la ejecución de múltiples núcleos o múltiples subprocesos. En el modelo JMM, al modificar valores de variables a través de subprocesos concurrentes, las variables de subprocesos deben sincronizarse. Sólo después de regresar a la memoria principal otros subprocesos pueden acceder a ella.

La ordenación proporciona el orden de acceso dentro de la memoria. Cuando diferentes programas acceden a diferentes bloques de memoria, el acceso no está desordenado. Por ejemplo, hay un bloque de memoria al que A y B necesitan acceder en ese momento. , JMM proporcionará una determinada estrategia de asignación de memoria para asignar la memoria que utilizan de manera ordenada, y el proceso de llamada de memoria también se volverá ordenado. La naturaleza comprometida de la memoria puede entenderse simplemente como orden. En programas Java multiproceso, JMM utiliza la palabra clave Java volátil para garantizar un acceso ordenado a la memoria. 1) Análisis simple:

Como se menciona en la especificación del lenguaje Java, hay un área de memoria principal (memoria principal o memoria dinámica de Java) en la JVM. Todas las variables en Java se almacenan en la memoria principal. Todos los subprocesos comparten y cada subproceso tiene su propia memoria de trabajo. Lo que se almacena en la memoria de trabajo es una copia de algunas variables en la memoria principal. Las operaciones de todas las variables por parte del subproceso no ocurren en la memoria principal. En cambio, ocurre en la memoria de trabajo y los subprocesos no pueden acceder directamente entre sí. La transferencia de variables en el programa depende de la memoria principal. En los procesadores multinúcleo, la mayoría de los datos se almacenan en la memoria caché. Si la memoria caché no pasa por la memoria, también es invisible. En los programas Java, la memoria en sí es un recurso relativamente costoso. De hecho, no solo para las aplicaciones Java, sino también para el propio sistema operativo, existen varias fuentes controlables típicas de sobrecarga de rendimiento. La visibilidad del modelo en memoria proporcionada por las palabras clave sincronizadas y volátiles garantiza que el programa utilice una instrucción de barrera de memoria especial para actualizar el caché, invalidar el caché, actualizar el caché de escritura del hardware y retrasar el proceso de entrega de ejecución. cierto impacto en el rendimiento de los programas Java.

El propósito original de JMM es admitir la programación multiproceso. Se puede considerar que cada subproceso se ejecuta en una CPU diferente de otros subprocesos, o para máquinas multiprocesador, este modelo requiere Lo que se logra es. para hacer que cada subproceso se ejecute como si se estuviera ejecutando en una máquina diferente, una CPU diferente o un subproceso diferente. Esta situación es realmente común en el desarrollo de proyectos.

Para la CPU en sí, no puede acceder directamente a los registros de otras CPU. El modelo debe usar ciertas reglas de definición para permitir que los subprocesos y los subprocesos se llamen entre sí en la memoria de trabajo para implementar la CPU en otras CPU o los subprocesos en la memoria de. otros subprocesos acceden a recursos, y el entorno de ejecución que expresa esta regla es generalmente el entorno del host en ejecución (sistema operativo, servidor, sistema distribuido, etc.) donde se ejecuta el programa, y ​​​​el rendimiento del programa en sí depende de las características del lenguaje. del programa, que está aquí Se dice que la implementación de la gestión de memoria en aplicaciones escritas en Java sigue algunos de sus principios. Es decir, el JMM mencionado anteriormente define algunas reglas relevantes del lenguaje Java para la memoria. Sin embargo, aunque fue diseñado originalmente para admitir mejor subprocesos múltiples, la aplicación y la implementación de este modelo, por supuesto, no se limitan a multiprocesadores. Ocurre cuando el compilador JVM compila un programa escrito en Java y cuando el programa se ejecuta durante. runtime. , esta regla también es válida para sistemas de una sola CPU. Esta es la estrategia de memoria entre subprocesos mencionada anteriormente. JMM en sí no mencionó la dirección de memoria específica en el proceso de descripción y el mecanismo proporcionado por el cual se implementa el enlace de la JVM (compilador, procesador, controlador de caché, etc.) al implementar la estrategia, e incluso para Un programador que está muy familiarizado Con el desarrollo, es posible que no necesariamente pueda comprender algunas de sus estructuras físicas visibles internas específicas de clases, objetos, métodos y contenido relacionado. Por el contrario, JMM define una relación abstracta entre un subproceso y la memoria principal. De hecho, de la figura anterior podemos saber que cada subproceso se puede abstraer en una memoria de trabajo (caché abstracta y registro), que almacena algunos valores. de Java Este modelo garantiza que las propiedades, métodos y campos en Java tengan ciertas características matemáticas. De acuerdo con estas características, el modelo almacena parte del contenido correspondiente y realiza ciertas operaciones de serialización y clasificación de almacenamiento en estos contenidos, de modo que Java lo objete. es llamado con éxito por la JVM en la memoria de trabajo (por supuesto, esta es una explicación más abstracta. En este caso, cuando se implementan la mayoría de las reglas de JMM, la comunicación entre la memoria principal y la memoria de trabajo debe garantizarse y no puede violarse). La estructura del modelo de memoria en sí es un método de diseño para la memoria que debe considerarse al diseñar el lenguaje. Una cosa que debe saber aquí es que todas estas operaciones en el lenguaje Java dependen del lenguaje Java en sí, porque para los desarrolladores, Java tiene su propia estrategia de administración de memoria sin operaciones manuales, lo cual también es una ventaja de Java. gestión de la memoria.

[1] Atomicidad:

Esto muestra que las reglas definidas por el modelo tienen un impacto independiente en el contenido a nivel atómico. Para el diseño inicial del modelo, estas reglas necesitan Qué. Lo que se describe son solo las operaciones más simples de lectura y escritura en unidades de almacenamiento. Este nivel atómico incluye instancias, variables estáticas, elementos de matriz, pero las variables locales en los métodos no están incluidas en esta regla.

[2] Visibilidad:

Bajo las restricciones de esta regla, define las circunstancias bajo las cuales un hilo puede acceder a otro hilo o afectar a otro hilo. Desde la perspectiva de la operación de JVM. incluye leer datos relevantes del área visible de otro hilo y escribir los datos en otro hilo.

[3] Orden:

Esta regla restringirá el orden de cualquier hilo llamado en violación de las reglas. El problema de orden gira principalmente en torno a secuencias relacionadas con lectura, escritura y asignación. declaraciones.

Cuando se utiliza una sincronización consistente dentro del modelo, cada una de estas propiedades sigue un principio relativamente simple: como todos los bloques de memoria sincronizados, Cualquier cambio es atómico y visible, y sigue los mismos principios que otros métodos de sincronización y bloques de sincronización. En tal modelo, cada bloque de sincronización no puede usar el mismo bloqueo. El proceso de llamada de todo el programa está de acuerdo con Los programas están escritos con instrucciones específicas para ejecutar.

Incluso si el procesamiento dentro de un determinado bloque sincronizado puede fallar, el problema no afectará la sincronización de otros subprocesos ni provocará fallas en la cadena. En pocas palabras: cuando el programa utiliza una sincronización consistente cuando se ejecuta, cada bloque de sincronización tiene un espacio independiente y un controlador de sincronización y un mecanismo de bloqueo independientes, y luego lee y escribe datos externamente de acuerdo con las instrucciones de ejecución de la JVM. ¡Esta situación hace que el proceso de uso de la memoria sea muy riguroso!

Si no se utiliza la sincronización o se utiliza de forma inconsistente (esto puede entenderse como una operación asincrónica, pero no necesariamente asincrónica), la respuesta a la ejecución del programa se volverá extremadamente complicada. Y en tales circunstancias, los resultados del procesamiento de este modelo de memoria se vuelven muy frágiles que los resultados esperados por la mayoría de los programadores, e incluso mucho más frágiles que la implementación proporcionada por la JVM. Debido a esto, Java tiene la especificación de lenguaje más simple para que esta operación de memoria imponga ciertas restricciones habituales. La forma de excluir esta situación es:

El subproceso JVM debe confiar en sí mismo para mantener la visibilidad del objeto y. El objeto en sí debe proporcionar las operaciones correspondientes para realizar las tres características de toda la operación de la memoria, en lugar de depender simplemente de un hilo específico que modifica el estado del objeto para completar un proceso tan complejo.

[4] Análisis de tres características (para componentes internos de JMM):

Atomicidad:

Accede a cualquier tipo de campo en la unidad de almacenamiento al realizar valor y actualizar operaciones, excepto el tipo largo y el tipo doble, otros tipos de campos deben garantizar su atomicidad. Estos campos también incluyen referencias que sirven a objetos. Además, la extensión de la regla de atomicidad se puede extender a otros dos tipos basados ​​en long y double: volatile long y volatile double (volatile es la palabra clave de Java), el tipo long que no se declara volátil y el valor de campo del tipo double son Sin embargo, no se garantiza la atomicidad en JMM. Cuando se utilizan campos no largos/no dobles en expresiones, la atomicidad de JMM tiene la siguiente regla: si obtiene o inicializa el valor o ciertos valores, estos valores son escritos por otros hilos, y cuando los datos se generan a partir de dos o más subprocesos se mezclan y escriben en la misma marca de tiempo, la atomicidad de este campo debe garantizarse dentro de la JVM. En otras palabras, cuando JMM define la atomicidad de JVM, siempre que no se viole la regla, a la JVM en sí no le importa de qué hilo proviene el valor de los datos, porque esto hace que el lenguaje Java esté en el proceso de diseño de operaciones paralelas. Diseñar atomicidad para subprocesos múltiples se vuelve extremadamente simple y no tiene mucho impacto en el programa final incluso si el desarrollador no lo tiene en cuenta. Explique nuevamente: la atomicidad aquí se refiere a operaciones de nivel atómico, como operaciones de lectura y escritura en la parte más pequeña de la memoria, que puede entenderse como la unidad operativa de nivel más bajo más cercana a la memoria después de que finalmente se compila el lenguaje Java. de operaciones de lectura y escritura La unidad de datos no es el valor de la variable, sino el código nativo, que es el código nativo generado cuando el corredor lo interpreta como se menciona en "Conceptos básicos de Java".

Visibilidad:

Cuando un hilo necesita modificar la unidad visible de otro hilo, debe seguir los siguientes principios: Un bloqueo de sincronización liberado por un hilo de escritura e inmediatamente seguido por la sincronización. El bloqueo del hilo de lectura que realiza la lectura es esencialmente el mismo, la operación de liberación del bloqueo fuerza a su hilo subordinado a liberar el bloqueo. El hilo que libera el bloqueo se actualiza desde la memoria caché de escritura en la memoria de trabajo (técnicamente hablando, esto no debería hacerlo). Al ser una actualización, puede entenderse como proporcionar) datos (operación de vaciado) y luego adquirir la operación de bloqueo para que otro hilo que obtenga el bloqueo lea directamente el valor del campo en el dominio accesible (es decir, el área visible). ) del hilo anterior.

Debido a que se proporciona un método de sincronización o un bloque de sincronización dentro del bloqueo, el contenido de sincronización es exclusivo del subproceso, por lo que las dos operaciones anteriores solo se pueden realizar dentro del contenido de sincronización para un solo subproceso, de modo que todos los subprocesos individuales que operan el contenido tienen El La alternancia de la exclusividad del hilo dentro del contenido de sincronización (método de sincronización bloqueado o bloque de sincronización) también puede entenderse como un "efecto de memoria a corto plazo". Lo que hay que entender aquí es el doble significado de sincronización: el uso de un mecanismo de bloqueo permite operaciones de procesamiento basadas en protocolos de sincronización de alto nivel, que es la sincronización más básica, al mismo tiempo, la memoria del sistema (muchas veces se refiere a; la barrera de memoria de nivel de almacenamiento subyacente basada en instrucciones de la máquina, la premisa es (he estado aquí antes) Al procesar la sincronización, puede operar entre subprocesos, de modo que los datos entre subprocesos estén sincronizados. Este mecanismo también refleja que la programación paralela es más similar a la programación distribuida que a la programación secuencial. La última sincronización se puede utilizar como demostración del efecto de un método en el mecanismo JMM que se ejecuta en un subproceso. Tenga en cuenta que esto no es una demostración del efecto de la ejecución de varios subprocesos, porque refleja las operaciones duales que el subproceso está dispuesto. para enviar o aceptar, y hacer su propia El área visible se puede proporcionar para que otros subprocesos se ejecuten o actualicen. Desde esta perspectiva, el uso de bloqueos y el paso de mensajes puede considerarse como una sincronización variable entre sí, porque sus operaciones son equivalentes a. otros hilos en relación con otros hilos. Una vez que un campo se declara volátil, se requieren más operaciones de memoria antes de que cualquier hilo de escritura actualice el caché en la memoria de trabajo. Es decir, dicho campo se actualiza inmediatamente. Se puede entender que este tipo de volátil no aparecerá. operación de almacenamiento en caché de variables, y el hilo de lectura debe volver a leer el valor de la variable de acuerdo con el dominio visible del hilo anterior cada vez, en lugar de leerlo directamente. Cuando un hilo accede al dominio de un objeto por primera vez, inicializa el valor del objeto o lee el valor del objeto del dominio visible de otros hilos de escritura combinado con el entendimiento anterior, debe satisfacer un cierto Bajo; En estas condiciones, el hilo lee el valor de un campo de objeto directamente, pero a veces es necesario volver a leerlo. Una cosa a tener en cuenta aquí es que en la programación concurrente, una mala práctica es utilizar una referencia legal para referirse a un objeto construido de forma incompleta. Esta situación ocurre con frecuencia cuando se leen datos del dominio visible de otros subprocesos de escritura. Desde una perspectiva de programación, iniciar un nuevo hilo en el constructor es arriesgado, especialmente cuando la clase es una clase subclasificable. Thread.start lo inicia el subproceso que llama y luego el subproceso que obtuvo el inicio libera el bloqueo. Tiene el mismo "efecto de memoria a corto plazo" si una superclase que implementa la interfaz Runnable llama a Thread(this).start. antes de que se ejecute el método constructor de la subclase (), entonces es posible que el objeto no se haya inicializado por completo antes de que se ejecute el método del subproceso, lo que provocará que una referencia legal al objeto se refiera a un objeto construido de forma incompleta. De manera similar, si crea un nuevo hilo T e inicia el hilo, y luego usa el hilo T para crear un objeto, sincronice la referencia del objeto, o la mejor manera es crear el objeto X antes de que comience el hilo T. Si un hilo termina, todos los valores de las variables deben vaciarse de la memoria de trabajo a la memoria principal. Por ejemplo, si un hilo de sincronización termina debido a que otro hilo usa el método Thread.join, entonces el alcance visible del hilo es para ese hilo. Es necesario conocer los cambios que se han producido y algunos de sus impactos. Nota: Si pasa una referencia a un objeto a través de una llamada a un método en el mismo hilo, el problema de visibilidad mencionado anteriormente nunca ocurrirá.

JMM garantiza todas las disposiciones anteriores y la descripción de las características de visibilidad de la memoria: una actualización especial, una modificación de un campo específico es un concepto de "visibilidad" de un hilo para otros hilos y, en última instancia, el lugar donde ocurre es en la memoria. El tiempo de aparición de subprocesos Java en el modelo puede ser arbitrariamente largo, pero eventualmente sucederá. En otras palabras, la característica de visibilidad en el modelo de memoria Java es principalmente para el uso de memoria entre subprocesos. definido por JMM.

No sólo eso, este modelo también permite funciones de visibilidad sin sincronización. Por ejemplo, un subproceso proporciona un objeto o el valor original de un campo de acceso a un campo para su operación, y otro subproceso proporciona un objeto o el valor actualizado de un campo para su operación. También es posible leer un valor original y el contenido del objeto de referencia para un hilo, y leer un valor actualizado o una referencia actualizada para otro hilo.

Sin embargo, algunas características del análisis de visibilidad anterior pueden fallar cuando se opera entre subprocesos, y estas fallas no se pueden evitar. Este es un hecho indiscutible. El uso de código multiproceso sincronizado no garantiza absolutamente un comportamiento seguro para los subprocesos, pero solo permite que ciertas reglas impongan ciertas restricciones a sus operaciones. Sin embargo, en la última implementación de JVM y en la última plataforma Java, incluso. Para múltiples procesadores, las pruebas de visibilidad utilizando algunas herramientas han revelado que rara vez ocurren fallas. La desventaja de utilizar el caché compartido entre subprocesos para compartir la CPU es que afecta las operaciones de optimización del compilador. Esto también refleja que una fuerte coherencia del caché aumenta el valor del hardware, porque entre ellos La complejidad de las relaciones entre subprocesos aumenta. Este enfoque hace que las pruebas gratuitas de visibilidad sean aún más imprácticas, porque la aparición de estos errores es extremadamente rara o, en otras palabras, no podemos encontrarlos en absoluto durante el proceso de desarrollo en la plataforma. En el desarrollo paralelo, las fallas causadas por no usar la sincronización no solo se deben a un control deficiente de la visibilidad. Hay muchas razones para las fallas del programa, incluida la coherencia de la caché, problemas de coherencia de la memoria, etc.

Ordenación:

Las reglas de ordenación incluyen principalmente los dos puntos siguientes entre subprocesos: desde la perspectiva de los subprocesos operativos, si todas las instrucciones se ejecutan en el orden normal, entonces para un programa Cuando se ejecuta secuencialmente, la ordenación también es secuencial. Desde la perspectiva de otros subprocesos operativos, la ordenación es como un "espía" que se ejecuta en un método asincrónico en este hilo. La única restricción útil es el orden relativo de los métodos sincronizados y los bloques sincronizados, que siempre está reservado para operaciones como campos volátiles *: ¿Cómo entender el significado de "espía" aquí? ¿Se puede entender que se siguen las reglas de orden en esto? subproceso.Una regla, pero para otros subprocesos, las características de orden de un subproceso pueden hacer que acceda indefinidamente al dominio visible del subproceso de ejecución, lo que hace que el subproceso tenga un cierto impacto en el subproceso que se está ejecutando. Por ejemplo, el subproceso A necesita hacer tres cosas, a saber, A1, A2 y A3, y B es otro subproceso con operaciones B1 y B2. Si la referencia se coloca en el subproceso B, entonces para el subproceso A, la operación B1 de B es. B2 puede acceder al área visible de A en cualquier momento. Por ejemplo, A tiene un área visible a y A1 modifica a para que sea 1, pero el hilo B accede a a y usa B1 o B2 después de que el hilo A llama a A1. La operación hace que a cambie y se convierta en 2. Luego, cuando A realiza la operación A2 de acuerdo con el orden para leer el valor de a, lee 2 en lugar de 1. Esto hace que el hilo A cuando se diseñó originalmente el programa. ha cambiado, es decir, la clasificación se ha interrumpido, entonces la identidad del hilo B es un "espía" para el hilo A, y cabe señalar que estas operaciones del hilo B no tendrán una relación de espera con el hilo A. Entonces estas operaciones de El subproceso B son operaciones asincrónicas, por lo que para la ejecución del subproceso A, la identidad de B es el "espía" en el método asincrónico.

Nuevamente, esto es solo una garantía mínima. En cualquier programa o plataforma, el desarrollo puede encontrar ordenamientos más estrictos, pero los desarrolladores no pueden confiar en esto al diseñar programas. Si confías en ellos, lo encontrarás. la dificultad de las pruebas aumentará exponencialmente y, al combinar regulaciones, diferentes características harán que la implementación de JVM falle porque no cumple con la intención del diseño original.

Nota: El primer punto también se adopta en todas las discusiones sobre JLS (especificación del lenguaje Java). Por ejemplo, las expresiones aritméticas generalmente se ordenan de arriba a abajo y de izquierda a derecha, pero esto es necesario. Lo que debe entenderse es que esto es incierto desde la perspectiva de otros subprocesos operativos. Dentro del subproceso, el modelo de memoria en sí está ordenado. *: La clasificación discutida aquí es la clasificación de NativeCode cuando se ejecuta en la memoria más baja, no significa la naturaleza ordenada del código Java ejecutado en secuencia. Este artículo analiza principalmente el modelo de memoria de JVM, por lo que espero que los lectores comprendan lo que significa. aquí la unidad de discusión para la generación es el área de la memoria. Hubo ciertas fallas en el diseño inicial de JMM. Aunque la plataforma JVM existente ha reparado estas fallas, deben mencionarse aquí para que los lectores comprendan mejor las ideas de diseño de JMM. Los conceptos de esta sección pueden involucrar muchos más. Conocimiento profundo, si el lector no puede entenderlo, no importa. Puede leer los capítulos al final del artículo y luego volver a leerlo.

1) Pregunta 1: Los objetos inmutables no son inmutables

Los amigos que hayan estudiado Java deberían conocer los objetos inmutables en Java. Este punto se explicará al final de este artículo sobre. Clase de cadena También se mencionará a veces, y cuando se diseñó originalmente JMM, este problema siempre ha existido, es decir: los objetos inmutables parecen poder cambiar sus valores (la inmutabilidad de dichos objetos está garantizada mediante el uso del final). palabra clave), (Recordatorio del servicio Publis: hacer que todos los campos de un objeto sean finales no necesariamente hace que el objeto sea inmutable; todos los tipos también deben ser tipos primitivos y no referencias a objetos. Sin embargo, no se considera que los objetos inmutables requieran sincronización. Hay una Retraso potencial en la propagación de cambios de escritura en memoria de un subproceso a otro, lo que hace posible tener una condición de carrera que permita a un subproceso ver primero un valor de un objeto inmutable durante un período de tiempo. Lo que ve es un valor diferente. ¿Cómo hizo esto? ¿Qué pasó antes? En la implementación de String en JDK 1.4, hay básicamente tres campos decisivos importantes: la referencia a la matriz de caracteres, la longitud y el comienzo de la cadena. El desplazamiento de una cadena se implementó en JDK 1.4. , en lugar de solo matrices de caracteres, por lo que las matrices de caracteres se pueden compartir entre múltiples objetos String y StringBuffer sin necesidad de crearlas cada vez. Se copia una cadena en una nueva matriz de caracteres:

String. s1 = /usr/tmp;

String s2 = s1.substring(4 ); // /tmp

En este caso, la cadena s2 tendrá una longitud y un desplazamiento de tamaño 4. , pero compartirá el contenido en "/usr/tmp" con s1*** Para la misma matriz de caracteres, antes de que se ejecute el constructor de cadenas, el constructor de objetos inicializará todos los campos con sus valores predeterminados, incluidos los campos decisivos de longitud y desplazamiento. Cuando se ejecuta el constructor String, la longitud y el desplazamiento de la cadena se inicializarán en el valor requerido.

Pero en el modelo de memoria antiguo, debido a la falta de sincronización, era posible que otro subproceso viera temporalmente el campo de compensación con el valor predeterminado inicial de 0 y luego viera el valor correcto de 4, lo que provocaba que el valor de s2 cambiara. de "/usr" se convirtió en "/tmp", que no es nuestra verdadera intención original. Este problema es el primer defecto del JMM original, porque esto es razonable y legal en el modelo JMM original, y las versiones inferiores a JDK 1.4 lo permiten. Hacer.

2) Problema 2: Reordenamiento del almacenamiento volátil y no volátil

Otra área importante está relacionada con el reordenamiento de las operaciones de memoria en campos volátiles. En esta área, existen algunos JMM. han causado algunos resultados confusos. Los JMM existentes indican que las lecturas y escrituras volátiles tratan directamente con la memoria principal, evitando así almacenar valores en registros o eludir cachés específicos del procesador. Esto permite que varios subprocesos vean generalmente el valor más reciente de una variable determinada. Sin embargo, resultó que esta definición de volátil no funcionó como se imaginó originalmente y generó una gran confusión sobre lo volátil. Para proporcionar un mejor rendimiento en ausencia de sincronización, los compiladores, tiempos de ejecución y cachés a menudo permiten operaciones de reordenamiento de la memoria siempre que el subproceso que se está ejecutando actualmente no pueda notar la diferencia. (Esto es lo que significa la semántica dentro del subproceso como si fuera en serie). Sin embargo, las lecturas y escrituras volátiles se programan completamente entre subprocesos y el compilador o la memoria caché no pueden reordenarlas entre sí. Desafortunadamente, JMM permite reordenar las lecturas y escrituras volátiles haciendo referencia a lecturas y escrituras de variables ordinarias, de modo que los desarrolladores no pueden usar el indicador volátil como señal de que la operación se ha completado. Por ejemplo:

Map configOptions;

char[] configText;

volatile boolean inicializado = false

//Thread 1<; /p> p>

configOptions = new HashMap();

configText = readConfigFile(nombre de archivo);

processConfigOptions(configText, configOptions);

inicializado = verdadero;

// Hilo 2

mientras(!inicializado)

sleep();

La idea aquí es utilizar volatilidad La variable inicializada actúa como protección para indicar que se ha completado un conjunto de otras operaciones. Esta es una buena idea, pero no funciona en JMM porque el antiguo JMM permite escrituras no volátiles (como escribir en configOptions). (campo y escritura en los campos en el mapa al que hace referencia configOptions) se reordenan junto con las escrituras volátiles, por lo que otro hilo podría verse inicializado como verdadero, pero aún no hay uno consistente o actual para el campo configOptions o el objeto al que hace referencia. Para las variables de vista de memoria, la antigua semántica de volátil solo promete la visibilidad de las variables durante la lectura y la escritura, pero no otras variables. Aunque este método es más eficiente de implementar, los resultados serán muy diferentes del diseño original.