Descripción general del modelo de memoria Java (JMM)

Modelo de memoria de Java (modelo de memoria de Java)

Transferencia de jenkov

El modelo de memoria Java especifica cómo se usa la máquina virtual Java con la memoria de la computadora (RAM). La máquina virtual Java es un modelo de toda la computadora, por lo que el modelo incluye naturalmente un modelo de memoria, también conocido como modelo de memoria Java.

Si desea diseñar correctamente un programa con comportamiento concurrente, es muy importante comprender el modelo de memoria de Java. El modelo de memoria de Java especifica cómo y cuándo diferentes subprocesos ven los valores escritos en variables compartidas por otros subprocesos, y cómo sincronizar el acceso a variables compartidas cuando sea necesario.

El modelo de memoria de Java original era insuficiente, por lo que el modelo de memoria de Java se revisó en Java 1.5. Esta versión del modelo de memoria de Java todavía se usa en Java (Java 14+) en la actualidad.

Modelo de memoria interna de Java

El modelo de memoria de Java utilizado dentro de la JVM asigna memoria entre la pila de subprocesos y el montón. Esta figura ilustra el modelo de memoria de Java desde una perspectiva lógica:

Inserte la descripción de la imagen aquí
Cada hilo que se ejecuta en la máquina virtual Java tiene su propia pila de hilos. La pila de subprocesos contiene información sobre los métodos que el subproceso llamó para alcanzar el punto de ejecución actual. Yo lo llamo "pila de llamadas". Cuando un hilo ejecuta su código, la pila de llamadas cambia.

La pila de subprocesos también contiene todas las variables locales de cada método que se está ejecutando (todos los métodos en la pila de llamadas). Un hilo solo puede acceder a su propia pila de hilos. Las variables locales creadas por un hilo son invisibles para todos los demás hilos excepto el hilo de creación. Incluso si el código ejecutado por los dos subprocesos es exactamente el mismo, los dos subprocesos seguirán creando variables locales del código en sus respectivas pilas de subprocesos. Por lo tanto, cada hilo tiene su propia versión de cada variable local.

Todas las variables locales de tipos primitivos (boolean, byte, short, char, int, long, float, double) se almacenan completamente en la pila de subprocesos, por lo que no son visibles para otros subprocesos. Un hilo puede pasar una copia de una variable principal a otro hilo, pero no puede compartir la propia variable local original.

El montón contiene todos los objetos creados en una aplicación Java, independientemente del hilo que creó el objeto. Esto incluye tipos primitivos (como versiones de objeto Byte, Integer, Long, etc.). No importa si crea un objeto y lo asigna a una variable local, o lo crea como una variable miembro de otro objeto, el objeto aún se almacena en el montón.

Este es un diagrama que ilustra la pila de llamadas, las variables locales almacenadas en la pila de subprocesos y los objetos almacenados en el montón:
Inserte la descripción de la imagen aquí
las variables locales pueden ser tipos primitivos, en cuyo caso permanece completamente en la pila de subprocesos.

Las variables locales también pueden ser referencias a objetos. En este caso, la referencia (variable local) se almacena en la pila de subprocesos, pero el objeto en sí (si se almacena en el montón).

Un objeto puede contener métodos y estos métodos pueden contener variables locales. Estas variables locales también se almacenan en la pila de subprocesos, incluso si el objeto al que pertenece el método está almacenado en el montón.

Las variables miembro del objeto se almacenan en el montón junto con el objeto en sí. Esto es cierto cuando la variable miembro es un tipo primitivo y cuando es una referencia a un objeto.

Las variables de clase estáticas también se almacenan en el montón junto con la definición de clase.

Todos los subprocesos que hacen referencia al objeto pueden acceder al objeto en el montón. Cuando un hilo puede acceder a un objeto, también puede acceder a las variables miembro del objeto. Si dos subprocesos llaman a un método en el mismo objeto al mismo tiempo, ambos tendrán acceso a las variables miembro del objeto, pero cada subproceso tendrá su propia copia de las variables locales.

Este es un diagrama que ilustra los puntos anteriores:
Inserte la descripción de la imagen aquí
dos subprocesos tienen un conjunto de variables locales. Una de las variables locales (Variable local 2) apunta a un objeto compartido (Objeto 3) en el montón. Cada uno de estos dos hilos tiene diferentes referencias al mismo objeto. Sus referencias son variables locales, por lo que se almacenan en la pila de subprocesos de cada subproceso (en cada subproceso). Sin embargo, dos referencias diferentes apuntan al mismo objeto en el montón.

Observe cómo el objeto compartido (objeto 3) hace referencia al objeto 2 y al objeto 4 como variables miembro (como lo muestran las flechas del objeto 3 al objeto 2 y al objeto 4). A través de estas referencias de variable miembro en el objeto 3, dos subprocesos pueden acceder al objeto 2 y al objeto 4.

La figura también muestra una variable local que apunta a dos objetos diferentes en el montón. En este caso, la referencia apunta a dos objetos diferentes (objeto 1 y objeto 5), no al mismo objeto. En teoría, si dos subprocesos hacen referencia a dos objetos, ambos subprocesos pueden acceder al objeto 1 y al objeto 5. Pero en la figura anterior, cada hilo hace referencia solo a uno de los dos objetos.

Entonces, ¿qué tipo de código Java podría causar el gráfico de memoria anterior? Bueno, el código es tan simple como el siguiente código:

public class MyRunnable implements Runnable() {
    
    

    public void run() {
    
    
        methodOne();
    }

    public void methodOne() {
    
    
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
    
    
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}

public class MySharedObject {
    
    

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member2 = 67890;
}

Si dos subprocesos están ejecutando el método run (), el resultado será el mismo que antes. El método run () llama a methodOne () y methodOne () llama a methodTwo ().

methodOne () declara una variable local básica (localVariable1 tipo int) y una variable local, que es una referencia de objeto (localVariable2).

Cada hilo que ejecuta methodOne () creará su propia copia, localVariable1 y localVariable2 en su propia pila de hilos. Estas variables localVariable1 estarán completamente separadas entre sí y solo existirán en la pila de subprocesos de cada subproceso. Un hilo no puede ver los cambios realizados por otro hilo en su copia localVariable1.

Cada hilo de ejecución methodOne () también creará su propia copia de localVariable2. Sin embargo, dos copias diferentes de las dos localVariable2 terminan apuntando al mismo objeto en el montón. Este código establece localVariable2 para que apunte al objeto al que hace referencia la variable estática. Solo hay una copia de una variable estática y esta copia se almacena en el montón. Por lo tanto, ambas copias finales de localVariable2 apuntan a la misma instancia apuntada por la variable estática MySharedObject. La instancia de MySharedObject también se almacena en el montón. Corresponde al objeto 3 en la figura anterior.

Tenga en cuenta que la clase MySharedObject también contiene dos variables miembro. La propia variable miembro se almacena en el montón junto con el objeto. Estas dos variables miembro apuntan a otros dos objetos Integer. Estos objetos Integer corresponden al objeto 2 y al objeto 4 en la figura anterior.

También observe cómo methodTwo () crea una variable local llamada localVariable1. Esta variable local es un entero de referencia de objeto al objeto. Este método establece la referencia localVariable1 para que apunte a la nueva instancia de Integer. La referencia localVariable1 se almacenará en una copia de methodTwo () en cada hilo de ejecución. Los dos objetos instanciados por Integer se almacenarán en el montón, pero dado que el método Integer creará un nuevo objeto cada vez que se ejecute, los dos subprocesos que ejecutan este método crearán instancias de Integer separadas. El objeto methodTwo () creado dentro de Integer corresponde al objeto 1 y al objeto 5 en la figura anterior.

También tenga en cuenta que las dos variables miembro en la clase de tipo MySharedObject, siempre que sean tipos primitivos. Dado que estas variables son variables miembro, todavía se almacenan en el montón junto con el objeto. Solo las variables locales se almacenan en la pila de subprocesos.

Arquitectura de memoria de hardware

La arquitectura de memoria de hardware moderna es diferente del modelo de memoria interna de Java. También es importante comprender la arquitectura de la memoria de hardware y comprender cómo funciona el modelo de memoria Java con ella. Esta sección describe arquitecturas de memoria de hardware comunes, y la siguiente sección describe cómo funciona el modelo de memoria de Java.

Este es un diagrama simplificado de la arquitectura moderna de hardware de computadora:
Inserte la descripción de la imagen aquí

Las computadoras modernas generalmente contienen 2 o más CPU. Algunas de estas CPU también pueden tener varios núcleos. El punto es que en las computadoras modernas con 2 o más CPU, se pueden ejecutar varios subprocesos simultáneamente. Cada CPU puede ejecutar un hilo en cualquier momento. Esto significa que si la aplicación Java tiene varios subprocesos, cada CPU puede ejecutar un subproceso en la aplicación Java al mismo tiempo (al mismo tiempo).

Cada CPU contiene un conjunto de registros, que son esencialmente memoria de CPU. La CPU puede realizar operaciones en estos registros mucho más rápido que las variables en la memoria principal. Esto se debe a que la CPU puede acceder a estos registros más rápido que la memoria principal.

Cada CPU también puede tener una capa de almacenamiento de caché de CPU. De hecho, la mayoría de las CPU modernas tienen un cierto tamaño de capa de caché. La CPU puede acceder a su caché más rápido que la memoria principal, pero generalmente no es tan rápido como puede acceder a los registros internos. Por lo tanto, la memoria caché de la CPU se encuentra entre los registros internos y la velocidad de la memoria principal. Algunas CPU pueden tener varias capas de caché (nivel 1 y nivel 2), pero no es importante comprender cómo interactúa el modelo de memoria Java con la memoria. Es importante saber que la CPU puede tener algún tipo de capa de caché.

La computadora también contiene un área de almacenamiento principal (RAM). Todas las CPU pueden acceder a la memoria principal. El área de almacenamiento principal suele ser mucho más grande que la caché de la CPU.

Por lo general, cuando la CPU necesita acceder a la memoria principal, lee parte de la memoria principal en su caché de la CPU. Incluso puede leer parte del caché en sus registros internos y luego realizar operaciones en él. Cuando la CPU necesita volver a escribir el resultado en la memoria principal, vaciará el valor de su registro interno al caché y luego vaciará el valor de nuevo a la memoria principal en algún momento.

Cuando la CPU necesita almacenar otro contenido en la caché, generalmente vacía el valor almacenado en la caché de nuevo a la memoria principal. El caché de la CPU puede escribir datos en parte de su memoria a la vez y actualizar parte de su memoria a la vez. No tiene que leer / escribir el caché completo cada vez que se actualiza. Generalmente, la caché se actualiza en bloques de memoria más pequeños llamados "líneas de caché". Se pueden leer una o más líneas de caché en la memoria caché, y una o más líneas de caché se pueden vaciar de nuevo a la memoria principal.

Cerrar la brecha entre el modelo de memoria Java y la arquitectura de memoria de hardware

Como se mencionó anteriormente, el modelo de memoria de Java y la arquitectura de memoria de hardware son diferentes. La arquitectura de la memoria de hardware no distingue entre pila de subprocesos y montón. En hardware, la pila de subprocesos y el montón se encuentran en la memoria principal. Parte de la pila y el montón de subprocesos pueden aparecer a veces en la memoria caché de la CPU y en los registros internos de la CPU. La siguiente figura ilustra esto:
Inserte la descripción de la imagen aquí
Cuando los objetos y las variables se pueden almacenar en varias áreas de almacenamiento de la computadora, pueden ocurrir ciertos problemas. Los dos problemas principales son:

  • Actualizaciones de subprocesos (escrituras) a la visibilidad de las variables compartidas.
  • Condiciones de carrera al leer, verificar y escribir variables compartidas.

Estos dos problemas se explicarán en las siguientes secciones.

Visibilidad de objetos compartidos

Si dos o más subprocesos comparten un objeto sin utilizar la declaración volátil o la sincronización correctamente, las actualizaciones realizadas por un subproceso en el objeto compartido pueden no ser visibles para otros subprocesos.

Imagine que el objeto compartido se almacena inicialmente en la memoria principal. Luego, el hilo que se ejecuta en la CPU lee el objeto compartido en su caché de la CPU. Allí, cambió la biblioteca compartida. Siempre que la memoria caché de la CPU no se vacíe de nuevo a la memoria principal, los subprocesos que se ejecutan en otras CPU no pueden ver la versión modificada del objeto compartido. De esta manera, cada subproceso puede eventualmente tener su propia copia de la biblioteca compartida, con cada copia en un caché de CPU diferente.

La siguiente figura ilustra esta situación. Un hilo que se ejecuta en la CPU izquierda copia la biblioteca compartida en su caché de CPU y cambia su variable de recuento a 2. Los otros subprocesos que se ejecutan en la CPU correcta no pueden ver este cambio porque el recuento no ha devuelto la actualización a la memoria principal.
Inserte la descripción de la imagen aquí

Para resolver este problema, puede utilizar la palabra clave volátil de Java . La palabra clave volátil puede garantizar que una variable determinada siempre se vuelva a escribir en la memoria principal cuando se lee y actualiza directamente desde la memoria principal.

Condiciones de competencia

Si dos o más subprocesos comparten un objeto y más de un subproceso actualiza variables en el objeto compartido, puede ocurrir una condición de anticipación .

Imagínese si el hilo A lee la variable de la biblioteca compartida count en su caché de CPU. Imagine también que el subproceso B tiene la misma función, pero está en una memoria caché de CPU diferente. Ahora, el hilo A agrega un recuento y el hilo B realiza la misma operación. Ahora var1 se ha aumentado dos veces, una por caché de CPU.

Si estos incrementos se realizan de forma secuencial, el recuento de variables se incrementará dos veces y el valor original + 2 se volverá a escribir en la memoria principal.

Sin embargo, estos dos incrementos se ejecutan simultáneamente sin una sincronización adecuada. No importa qué hilo A o B escriba su versión actualizada en la memoria principal, aunque hay dos incrementos, el valor actualizado es solo 1 más alto que el valor original.

La figura ilustra la aparición del problema de condición de carrera descrito anteriormente:
Inserte la descripción de la imagen aquí
Para resolver este problema, puede utilizar el bloque de sincronización de Java . El bloque de sincronización garantiza que solo un hilo pueda ingresar a una parte crítica dada del código en un momento dado. El bloque de sincronización también garantiza que todas las variables a las que se acceda en el bloque de sincronización se leerán de la memoria principal. Cuando el hilo sale del bloque de sincronización, todas las variables actualizadas volverán a la memoria principal, independientemente de si la variable se declara volátil.

Supongo que te gusta

Origin blog.csdn.net/e891377/article/details/108686228
Recomendado
Clasificación