Multithreading. O modelo de memória Java (parte 1)

Olá Habr! Apresento a sua atenção a tradução da primeira parte do artigo "Java Memory Model", de Jakob Jenkov.



Estou passando por treinamento em Java e precisava estudar o artigo Modelo de Memória Java . Traduzi-o para melhor compreensão, mas para que o bem não se perdesse, decidi compartilhá-lo com a comunidade. Eu acho que será útil para iniciantes e, se alguém gostar, eu traduzirei o resto.



O modelo de memória Java original não era muito bom, portanto foi revisado no Java 1.5. Esta versão ainda está em uso hoje (Java 14+).





Modelo de memória Java interno



O modelo de memória Java usado internamente pela JVM divide a memória em uma pilha de encadeamentos e um heap. Este diagrama ilustra o modelo de memória Java de um ponto de vista lógico:



imagem



Cada encadeamento em execução na máquina virtual Java possui sua própria pilha. A pilha contém informações sobre quais métodos o thread chamou para alcançar o ponto de execução atual. Vou me referir a isso como a "pilha de chamadas". Assim que o thread executa seu código, a pilha de chamadas muda.



A pilha de encadeamentos contém todas as variáveis ​​locais para cada método que é executado (todos os métodos na pilha de chamadas). Um encadeamento pode acessar apenas sua própria pilha. Variáveis ​​locais são invisíveis para todos os outros segmentos, exceto o segmento que os criou. Mesmo se dois threads estiverem executando o mesmo código, eles ainda criarão variáveis ​​locais desse código em suas próprias pilhas. Assim, cada encadeamento tem sua própria versão de cada variável local.



Todas as variáveis ​​locais de tipos primitivos (booleano, byte, curto, char, int, longo, flutuante, duplo) são completamente armazenadas na pilha de encadeamentos e não são visíveis para outros encadeamentos. Um encadeamento pode passar uma cópia de uma variável primitiva para outro encadeamento, mas não pode compartilhar uma variável local primitiva.



O heap contém todos os objetos criados no seu aplicativo Java, independentemente de qual thread criou o objeto. Isso inclui versões de objetos de tipos primitivos (por exemplo, Byte, Inteiro, Longo, etc.). Não importa se o objeto foi criado e atribuído a uma variável local ou criado como uma variável membro de outro objeto, ele é armazenado no heap.



Aqui está um diagrama que ilustra a pilha de chamadas e as variáveis ​​locais que são armazenadas nas pilhas de encadeamentos, bem como os objetos que são armazenados no heap: Uma



imagem



variável local pode ser do tipo primitivo; nesse caso, é completamente armazenada na pilha do encadeamento.



Uma variável local também pode ser uma referência de objeto. Nesse caso, a referência (variável local) é armazenada na pilha de encadeamentos, mas o próprio objeto é armazenado no heap.



Um objeto pode conter métodos, e esses métodos podem conter variáveis ​​locais. Essas variáveis ​​locais também são armazenadas na pilha de encadeamentos, mesmo se o objeto que possui o método estiver armazenado na pilha.



As variáveis ​​de membro de um objeto são armazenadas no heap junto com o próprio objeto. Isso ocorre tanto quando a variável de membro é de um tipo primitivo quanto quando é uma referência de objeto.



Variáveis ​​de uma classe estática também são armazenadas no heap junto com a definição de classe.



Os objetos no heap podem ser acessados ​​por todos os threads que têm uma referência ao objeto. Quando um encadeamento tem acesso a um objeto, ele também pode acessar as variáveis ​​de membro desse objeto. Se dois threads chamam um método no mesmo objeto ao mesmo tempo, ambos terão acesso às variáveis ​​de membro do objeto, mas cada thread terá sua própria cópia das variáveis ​​locais.



Aqui está um diagrama que ilustra os pontos acima:



imagem



Dois segmentos têm um conjunto de variáveis ​​locais. A variável local 2 aponta para um objeto compartilhado na pilha (objeto 3). Ou seja, cada um dos encadeamentos possui sua própria cópia da variável local com sua própria referência. Portanto, duas referências diferentes apontam para o mesmo objeto na pilha.



Observe que o objeto 3 genérico tem referências ao objeto 2 e ao objeto 4 como variáveis ​​de membro (mostradas pelas setas). Por meio desses links, dois encadeamentos podem acessar o Objeto 2 e o Objeto 4.



O diagrama também mostra a variável local (variável local 1). Cada cópia contém referências diferentes, que apontam para dois objetos diferentes (Objeto 1 e Objeto 5) e não para o mesmo. Em teoria, os dois threads podem acessar o Objeto 1 e o Objeto 5 se tiverem referências a esses dois objetos. Mas no diagrama acima, cada thread faz referência apenas a um dos dois objetos.



Então, que tipo de código Java pode ser o resultado dessas ilustrações? Bem, um código tão simples quanto o código abaixo:



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;
}


O método run () chama methodOne () e methodOne () chama methodTwo ().



methodOne () declara uma variável local primitiva (localVariable1) do tipo int e uma variável local (localVariable2) que é uma referência de objeto.



Cada thread que executa o método One () criará sua própria cópia de localVariable1 e localVariable2 em suas respectivas pilhas. As variáveis ​​localVariable1 serão completamente separadas uma da outra, estando na pilha de cada thread. Um thread não pode ver as alterações que outro thread está fazendo em sua cópia do localVariable1.



Cada thread que executa o método One () também cria sua própria cópia do localVariable2. No entanto, duas cópias diferentes do localVariable2 acabam apontando para o mesmo objeto no heap. O ponto é que localVariable2 aponta para o objeto referenciado pela variável estática sharedInstance. Há apenas uma cópia da variável estática e essa cópia é armazenada no heap. Assim, ambas as cópias do localVariable2 acabam apontando para a mesma instância MySharedObject. A instância MySharedObject também é armazenada na pilha. Corresponde ao Objeto 3 no diagrama acima.



Observe que a classe MySharedObject também contém duas variáveis ​​de membro. As próprias variáveis ​​de membro são armazenadas no heap junto com o objeto. As duas variáveis ​​de membro apontam para outros dois objetos Inteiros. Esses objetos inteiros correspondem ao Objeto 2 e ao Objeto 4 no diagrama.



Observe também que methodTwo () cria uma variável local denominada localVariable1. Essa variável local é uma referência a um objeto Inteiro. O método define a referência localVariable1 para apontar para uma nova instância Inteira. O link será armazenado em sua própria cópia de localVariable1 para cada thread. As duas instâncias de número inteiro serão armazenadas no heap e, como o método cria um novo objeto de número inteiro cada vez que é executado, os dois segmentos que executam esse método criarão instâncias de número inteiro separadas. Eles correspondem aos Objetos 1 e 5 no diagrama acima.



Observe também as duas variáveis ​​de membro na classe MySharedObject do tipo long, que é um tipo primitivo. Como essas variáveis ​​são variáveis ​​de membro, elas ainda são armazenadas no heap junto com o objeto. Somente variáveis ​​locais são armazenadas na pilha de encadeamentos.



A parte 2 está aqui.



All Articles