CodeGym/Cursos Java/Módulo 3/Memória na JVM, parte 2

Memória na JVM, parte 2

Disponível

Arquitetura de hardware de memória

A arquitetura de hardware de memória moderna difere do modelo de memória interna do Java. Portanto, você precisa entender a arquitetura de hardware para saber como o modelo Java funciona com ela. Esta seção descreve a arquitetura geral de hardware de memória e a próxima seção descreve como o Java funciona com ela.

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

Arquitetura de hardware de memória

No mundo moderno, um computador possui 2 ou mais processadores e isso já é a norma. Alguns desses processadores também podem ter vários núcleos. Nesses computadores, é possível executar vários threads ao mesmo tempo. Cada núcleo do processador é capaz de executar um thread a qualquer momento. Isso significa que qualquer aplicativo Java é a priori multiencadeado e, em seu programa, um encadeamento por núcleo do processador pode ser executado por vez.

O núcleo do processador contém um conjunto de registradores que residem em sua memória (dentro do núcleo). Ele executa operações em dados de registro muito mais rapidamente do que em dados que residem na memória principal do computador (RAM). Isso ocorre porque o processador pode acessar esses registros muito mais rapidamente.

Cada CPU também pode ter sua própria camada de cache. A maioria dos processadores modernos o possui. O processador pode acessar seu cache muito mais rápido que a memória principal, mas não tão rápido quanto seus registradores internos. O valor da velocidade de acesso à cache está aproximadamente entre as velocidades de acesso à memória principal e aos registos internos.

Além disso, os processadores têm um local para ter um cache multinível. Mas isso não é tão importante saber para entender como o modelo de memória Java interage com a memória do hardware. É importante saber que os processadores podem ter algum nível de cache.

Qualquer computador também contém RAM (área de memória principal) da mesma maneira. Todos os núcleos podem acessar a memória principal. A área da memória principal geralmente é muito maior que a memória cache dos núcleos do processador.

No momento em que 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 registradores internos e então executar operações neles. Quando a CPU precisar gravar o resultado de volta na memória principal, ela liberará os dados de seu registrador interno para o cache e, em algum momento, para a memória principal.

Os dados armazenados no cache normalmente são liberados de volta para a memória principal quando o processador precisa armazenar algo mais no cache. O cache tem a capacidade de limpar sua memória e gravar dados ao mesmo tempo. O processador não precisa ler ou gravar o cache completo todas as vezes durante uma atualização. Normalmente o cache é atualizado em pequenos blocos de memória, eles são chamados de "linha 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 hardware de memória

Como já 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 pilhas de threads e heaps. No hardware, a pilha de threads e o HEAP (heap) residem na memória principal.

Às vezes, partes de pilhas e pilhas de threads podem estar presentes em caches e registros internos da CPU. Isso é mostrado no diagrama:

pilha de threads e HEAP

Quando objetos e variáveis ​​podem ser armazenados em diferentes áreas da memória do computador, podem surgir alguns problemas. Aqui estão os dois principais:

  • Visibilidade das alterações que o encadeamento fez nas variáveis ​​compartilhadas.
  • Condição de corrida ao ler, verificar e escrever variáveis ​​compartilhadas.

Ambas as questões serão explicadas a seguir.

Visibilidade de objetos compartilhados

Se dois ou mais threads compartilham um objeto sem o uso adequado de declaração volátil ou sincronização, as alterações no objeto compartilhado feitas por um thread podem não ser visíveis para outros threads.

Imagine que um objeto compartilhado é inicialmente armazenado na memória principal. Um thread em execução em uma CPU lê o objeto compartilhado no cache da 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. Um thread em execução na CPU esquerda copia o objeto compartilhado em seu cache e altera o valor de contagem para 2. Essa alteração é invisível para outros threads em execução na CPU direita porque a atualização para contagem ainda não foi descarregada de volta para a memória principal.

Para resolver esse problema, você pode usar a palavra-chave volátil ao declarar uma variável. Ele pode garantir que uma determinada variável seja lida diretamente da memória principal e sempre escrita de volta na memória principal quando atualizada.

condição de corrida

Se dois ou mais threads compartilharem o mesmo objeto e mais de um thread atualizar variáveis ​​nesse objeto compartilhado, poderá ocorrer uma condição de corrida.

Imagine que a thread A leia a variável count do objeto compartilhado no cache de seu processador. Imagine também que a thread B faz a mesma coisa, mas no cache de outro processador. Agora o thread A adiciona 1 ao valor de contagem e o thread B faz o mesmo. Agora a variável foi aumentada duas vezes - separadamente em +1 no cache de cada processador.

Se esses incrementos fossem executados sequencialmente, a variável de contagem seria dobrada e escrita de volta na memória principal (valor original + 2).

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

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

Para resolver esse problema, você pode usar o bloco sincronizado Java. Um bloco sincronizado garante que apenas um thread possa 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 do bloco sincronizado serão lidas da memória principal e, quando a thread sair do bloco sincronizado, todas as variáveis ​​atualizadas serão descarregadas de volta para a memória principal, independentemente de a variável ser declarada volátil ou não.

Comentários
  • Populares
  • Novas
  • Antigas
Você precisa acessar para deixar um comentário
Esta página ainda não tem nenhum comentário