Multithreading. El modelo de memoria Java (Parte 1)

Hola Habr! Les presento a ustedes la traducción de la primera parte del artículo "Java Memory Model" de Jakob Jenkov.



Estoy entrenando en Java y necesitaba estudiar el artículo Modelo de memoria de Java . Lo traduje para una mejor comprensión, pero para que no se pierda lo bueno, decidí compartirlo con la comunidad. Creo que será útil para los principiantes, y si a alguien le gusta, traduciré el resto.



El modelo de memoria Java original no era muy bueno, por lo que se revisó en Java 1.5. Esta versión todavía está en uso hoy (Java 14+).





Modelo de memoria interna de Java



El modelo de memoria Java utilizado internamente por la JVM divide la memoria en una pila de subprocesos y un montón. Este diagrama ilustra el modelo de memoria Java desde un punto de vista lógico:



imagen



cada subproceso que se ejecuta en la máquina virtual Java tiene su propia pila. La pila contiene información sobre los métodos que el subproceso llamó para alcanzar el punto de ejecución actual. Me referiré a esto como la "pila de llamadas". Tan pronto como el hilo ejecuta su código, la pila de llamadas cambia.



La pila de subprocesos contiene todas las variables locales para cada método que se ejecuta (todos los métodos en la pila de llamadas). Un hilo solo puede acceder a su propia pila. Las variables locales son invisibles para todos los otros hilos excepto el hilo que los creó. Incluso si dos hilos están ejecutando el mismo código, seguirán creando variables locales para ese código en sus propias pilas. Por lo tanto, cada subproceso 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 y no son visibles para otros subprocesos. Un hilo puede pasar una copia de una variable primitiva a otro hilo, pero no puede compartir una variable local primitiva.



El montón contiene todos los objetos creados en su aplicación Java, independientemente de qué hilo haya creado el objeto. Esto incluye versiones de objetos de tipos primitivos (por ejemplo, Byte, Integer, Long, etc.). No importa si el objeto fue creado y asignado a una variable local o creado como una variable miembro de otro objeto, se almacena en el montón.



Aquí hay un diagrama que ilustra la pila de llamadas y las variables locales que se almacenan en las pilas de hilos, así como los objetos que se almacenan en el montón: Una



imagen



variable local puede ser de tipo primitivo, en cuyo caso se almacena completamente en la pila de hilos.



Una variable local también puede ser una referencia de objeto. En este caso, la referencia (variable local) se almacena en la pila de subprocesos, pero el objeto en sí 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 que posee el método se almacena en el montón.



Las variables miembro de un objeto se almacenan en el montón junto con el objeto mismo. Esto es cierto tanto cuando la variable miembro es de tipo primitivo como cuando es una referencia de objeto.



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



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



Aquí hay un diagrama que ilustra los puntos anteriores:



imagen



dos hilos tienen un conjunto de variables locales. La Variable local 2 apunta a un objeto compartido en el montón (Objeto 3). Es decir, cada uno de los hilos tiene su propia copia de la variable local con su propia referencia. Por lo tanto, dos referencias diferentes apuntan al mismo objeto en el montón.



Tenga en cuenta que el Objeto genérico 3 tiene referencias al Objeto 2 y al Objeto 4 como variables miembro (mostradas por flechas). A través de estos enlaces, dos hilos pueden acceder al Objeto 2 y al Objeto 4.



El diagrama también muestra la variable local (Variable local 1). Cada copia contiene referencias diferentes, que apuntan a dos objetos diferentes (Objeto 1 y Objeto 5), y no al mismo. En teoría, ambos hilos pueden acceder tanto al Objeto 1 como al Objeto 5 si tienen referencias a ambos objetos. Pero en el diagrama anterior, cada hilo solo hace referencia a uno de los dos objetos.



Entonces, ¿qué tipo de código Java podría ser el resultado de estas ilustraciones? Bueno, un código 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 {

    // ,    MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    // -,      

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

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


El método run () llama a methodOne () y methodOne () llama a methodTwo ().



methodOne () declara una variable local primitiva (localVariable1) de tipo int y una variable local (localVariable2) que es una referencia de objeto.



Cada subproceso que ejecuta el método One () creará su propia copia de localVariable1 y localVariable2 en sus respectivas pilas. Las variables localVariable1 estarán completamente separadas entre sí, estando en la pila de cada hilo. Un hilo no puede ver qué cambios está haciendo otro hilo en su copia de localVariable1.



Cada hilo que ejecuta el método One () también crea su propia copia de localVariable2. Sin embargo, dos copias diferentes de localVariable2 terminan apuntando al mismo objeto en el montón. El punto es que localVariable2 apunta al objeto al que hace referencia la variable estática sharedInstance. Solo hay una copia de la variable estática y esa copia se almacena en el montón. Por lo tanto, ambas copias de localVariable2 terminan apuntando a la misma instancia de MySharedObject. La instancia de MySharedObject también se almacena en el montón. Corresponde al Objeto 3 en el diagrama de arriba.



Tenga en cuenta que la clase MySharedObject también contiene dos variables miembro. Las variables miembro se almacenan en el montón junto con el objeto. Las dos variables miembro apuntan a otros dos objetos Integer. Estos objetos enteros corresponden al Objeto 2 y al Objeto 4 en el diagrama.



También tenga en cuenta que methodTwo () crea una variable local llamada localVariable1. Esta variable local es una referencia a un objeto Integer. El método establece la referencia localVariable1 para que apunte a una nueva instancia de Integer. El enlace se almacenará en su propia copia de localVariable1 para cada hilo. Las dos instancias de Integer se almacenarán en el montón y, dado que el método crea un nuevo objeto de Integer cada vez que se ejecuta, los dos subprocesos que ejecutan este método crearán instancias de Integer separadas. Corresponden al Objeto 1 y al Objeto 5 en el diagrama de arriba.



Observe también las dos variables miembro en la clase MySharedObject de tipo long, que es un tipo primitivo. Como 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 hilos.



La parte 2 está aquí.



All Articles