Multithreading. Modelo de memória Java (parte 2)

Olá, Habr! Apresento a sua atenção a tradução da segunda parte do artigo "Modelo de Memória Java" de Jakob Jenkov. A primeira parte está aqui .



Arquitetura de hardware de memória



A arquitetura de hardware de memória moderna é um pouco diferente do modelo de memória Java interno. É importante entender a arquitetura do hardware para entender como o modelo Java funciona com ele. Esta seção descreve a arquitetura geral do hardware de memória e a próxima seção descreve como Java funciona com ela.



Aqui está um diagrama simplificado da arquitetura de hardware de um computador moderno:



Um computador moderno geralmente tem 2 ou mais processadores. Alguns desses processadores também podem ter vários núcleos. Em tais computadores, é possível executar vários threads simultaneamente. Cada processador (nota do tradutor - doravante, o autor provavelmente se refere a um núcleo de processador ou um processador de núcleo único por um processador)capaz de executar um thread a qualquer momento. Isso significa que, se seu aplicativo Java for multithread, então, dentro de seu programa, um thread pode ser executado simultaneamente por processador.



Cada processador contém um conjunto de registradores que estão essencialmente em sua memória. Ele pode realizar operações em dados em registros muito mais rápido do que em dados que estão na memória principal do computador (RAM). Isso ocorre porque o processador pode acessar esses registros muito mais rápido.



Cada CPU também pode ter uma camada de cache. Na verdade, a maioria dos processadores modernos tem. Um processador pode acessar sua memória cache muito mais rápido do que a memória principal, mas geralmente não tão rápido quanto seus registros internos. Assim, a velocidade de acesso à memória cache está em algum lugar entre a velocidade de acesso aos registros internos e à memória principal. Alguns processadores podem ter caches em camadas, mas isso não é tão importante saber para entender como o modelo de memória Java interage com a memória de hardware. É importante saber que os processadores podem ter algum nível de memória cache.



O computador também contém uma área de memória principal (RAM). Todos os processadores podem acessar a memória principal. A área da memória principal geralmente é muito maior do que o cache do processador.



Normalmente, quando o processador precisa acessar a memória principal, ele lê parte dela em sua memória cache. Ele também pode ler alguns dados do cache em seus registros internos e, em seguida, executar operações neles. Quando a CPU precisa gravar um resultado de volta na memória principal, ela libera os dados de seu registro interno para a memória cache e, em algum ponto, para a memória principal.



Os dados armazenados no cache geralmente são descarregados de volta para a memória principal quando o processador precisa armazenar algo mais no cache. O cache pode limpar sua memória e gravar novos dados ao mesmo tempo. O processador não precisa ler / gravar o cache completo sempre que ele é atualizado. Normalmente, o cache é atualizado em pequenos blocos de memória chamados "linhas de cache". Uma ou mais linhas de cache podem ser lidas na memória cache e uma ou mais linhas de cache podem ser descarregadas de volta para a memória principal.



Combinando modelo de memória Java e arquitetura de memória de hardware



Conforme mencionado, o modelo de memória Java e a arquitetura de hardware de memória são diferentes. A arquitetura de hardware não faz distinção entre pilha e heap de threads. No hardware, a pilha e o heap do thread estão na memória principal. Porções de pilhas e pilhas de threads às vezes podem estar presentes em caches e registros internos da CPU. Isso é mostrado no diagrama:



Quando objetos e variáveis ​​podem ser armazenados em diferentes áreas da memória do computador, certos problemas podem surgir. Existem dois principais:

• Visibilidade das alterações feitas pelo thread nas variáveis ​​compartilhadas.

• Condições de corrida ao ler, verificar e gravar variáveis ​​compartilhadas.

Ambos os problemas serão explicados nas seções a seguir.



Visibilidade de objeto compartilhado



Se dois ou mais encadeamentos compartilham um objeto sem a declaração ou sincronização volátil apropriada, então as mudanças no objeto compartilhado feitas por um encadeamento podem não ser visíveis para outros encadeamentos.



Imagine que um objeto compartilhado seja inicialmente armazenado na memória principal. Um thread em execução em uma CPU lê um objeto compartilhado no cache dessa mesma CPU. Lá, ele faz alterações no objeto. Até que o cache da CPU seja liberado para a memória principal, a versão modificada do objeto compartilhado não é visível para threads em execução em outras CPUs. Assim, cada thread pode obter sua própria cópia do objeto compartilhado, cada cópia estará em um cache de CPU separado.



O diagrama a seguir ilustra um esboço dessa situação. Uma thread rodando na CPU esquerda copia o objeto compartilhado para seu cache e muda o valor da variávelcountpor 2. Esta alteração é invisível para outros threads em execução na CPU certa porque a atualização para ainda countnão foi descarregada de volta para a memória principal.



Para resolver este problema, você pode usar volatileao declarar uma variável. Ele pode garantir que uma determinada variável seja lida diretamente da memória principal e sempre gravada de volta na memória principal quando atualizada.



Condição de corrida



Se duas ou mais threads compartilham o mesmo objeto e mais de uma variável de atualização de thread nesse objeto compartilhado, pode ocorrer uma condição de corrida .



Imagine que o thread A está lendo uma variável de countobjeto compartilhado no cache de seu processador. Imagine também que o thread B está fazendo a mesma coisa, mas no cache de um processador diferente. Agora, o thread A adiciona 1 ao valor da variável counte o thread B faz o mesmo. Agora var1foi aumentado duas vezes - separadamente, +1 no cache de cada processador.



Se esses incrementos fossem executados sequencialmente, a variável countseria duplicada e gravada de volta na memória principal + 2.

No entanto, os dois incrementos foram executados simultaneamente sem a sincronização adequada. Independentemente de qual thread (A ou B) grava sua versão atualizada countna memória principal, o novo valor será apenas 1 a mais que o valor original, apesar de dois incrementos.



Este diagrama ilustra a ocorrência do problema de condição de corrida descrito acima:



Para resolver esse problema, você pode usar um bloco Java sincronizado... Um bloco sincronizado garante que apenas um thread pode entrar em uma determinada seção crítica do código a qualquer momento. Os blocos sincronizados também garantem que todas as variáveis ​​acessadas dentro de um bloco sincronizado sejam lidas da memória principal, e quando um thread sai de um bloco sincronizado, todas as variáveis ​​atualizadas serão descarregadas de volta para a memória principal, independentemente de a variável ser declarada como volatileou não. ...



All Articles