O Java Memory Model, referido como JMM, é uma garantia unificada para uma série de plataformas de máquina virtual Java para a plataforma específica não relacionada para a visibilidade da memória e se pode ser reordenada em um ambiente múltiplo fornecido pelos desenvolvedores. (Pode haver ambíguo em termos do termo e a distribuição de memória do tempo de execução do Java, que se refere a áreas de memória como heap, área de método, pilha de threads etc.).
Existem muitos estilos de programação simultânea. Além do CSP (processo sequencial de comunicação), ator e outros modelos, o mais familiar deve ser o modelo de memória compartilhada com base em threads e bloqueios. Na programação multithread, três tipos de problemas de simultaneidade precisam receber atenção a:
・ Atomicidade ・ Visibilidade ・ Reordene
A atomicidade envolve se outros encadeamentos podem ver o estado intermediário ou interferir quando um encadeamento executa uma operação composta. Normalmente, é o problema de I ++. Dois threads executam operações ++ na memória de heap compartilhada ao mesmo tempo. A implementação de operações ++ na JVM, Runtime e CPU pode ser uma operação composta. Por exemplo, da perspectiva das instruções da JVM, é ler o valor de I da memória da heap à pilha de operando, adicionar uma e escrever de volta à memória da heap i. Durante essas operações, se não houver sincronização correta, outros threads também poderão executá -la ao mesmo tempo, o que pode levar à perda de dados e outros problemas. Problemas comuns de atomicidade, também conhecidos como condição competitiva, são julgados com base em um possível resultado de falha, como a leitura-modificação. Os problemas de visibilidade e reordenação resultam da otimização do sistema.
Como a velocidade de execução da CPU e a velocidade de acesso da memória são seriamente incompatíveis, a fim de otimizar o desempenho, com base em princípios de localização, como localidade de tempo e localidade espacial, a CPU adicionou um cache de várias camadas entre a memória. Quando for necessário buscar dados, a CPU irá primeiro ao cache para descobrir se o cache correspondente existe. Se existir, será devolvido diretamente. Se não existir, será buscado na memória e salvo no cache. Agora, quanto mais os processadores de vários núcleos se tornaram padrão, cada processador tem seu próprio cache, que envolve a questão da consistência do cache. As CPUs têm modelos de consistência de diferentes forças e fraquezas. A consistência mais forte é a maior segurança e também está em conformidade com o nosso modo de pensamento seqüencial. No entanto, em termos de desempenho, haverá muita sobrecarga devido à necessidade de comunicação coordenada entre diferentes CPUs.
Um diagrama de estrutura de cache da CPU típico é o seguinte
O ciclo de instrução da CPU geralmente é buscar instruções, analisar instruções para ler dados, executar instruções e escrever dados de volta aos registros ou memória. Ao executar instruções em série, os dados de leitura e armazenamento ocupam muito tempo; portanto, a CPU geralmente usa o pipeline de instruções para executar várias instruções ao mesmo tempo para melhorar a taxa de transferência geral, como um pipeline de fábrica.
A velocidade da leitura de dados e a gravação de dados de volta à memória não está na mesma ordem de magnitude do que a execução de instruções, portanto a CPU usa registros e caches como caches e buffers. Ao ler dados da memória, ele lerá uma linha de cache (semelhante à leitura do disco e leia um bloco). O módulo que grava os dados de volta colocará a solicitação de armazenamento em um buffer de loja quando os dados antigos não estiverem no cache e continuará a executar o próximo estágio do ciclo de instruções. Se houver no cache, o cache será atualizado e os dados no cache serão lançados na memória de acordo com uma determinada política.
classe pública MemoryModel {private int count; parada booleana privada; public void initCountandStop () {count = 1; stop = false; } public void doloop () {while (! Stop) {count ++; }} public void PrintResult () {System.out.println (contagem); System.out.println (Stop); }}Ao executar o código acima, podemos pensar que a contagem = 1 será executada antes da parada = false. Isso está correto no estado ideal mostrado no diagrama de execução da CPU acima, mas está incorreto ao considerar o buffer de registro e cache. Por exemplo, o Stop em si está no cache, mas a contagem não está lá, então a parada pode ser atualizada e o buffer de gravação da contagem é atualizado na memória antes de escrever novamente.
Além disso, a CPU e o compilador (geralmente consulte o JIT para Java) podem modificar a ordem de execução de instruções. Por exemplo, no código acima, count = 1 e stop = false não têm dependências, portanto, a CPU e o compilador podem modificar a ordem desses dois. Na visão de um programa de thread único, o resultado é o mesmo. Este também é o serial se a CPU e o compilador devem garantir (independentemente de como a ordem de execução é modificada, o resultado da execução do thread único permanece inalterado). Como a maior parte da execução do programa é de thread única, essa otimização é aceitável e traz ótimas melhorias no desempenho. No entanto, no caso de multithreading, resultados inesperados podem ocorrer sem as operações de sincronização necessárias. Por exemplo, após o thread t1 executar o método initCountAndStop, o thread t2 executa o PrintResult, que pode ser 0, falso, 1, falso ou 0, true. Se o thread t1 executar o doloop () primeiro e o thread t2 executar o initcountandstop um segundo, o T1 poderá sair do loop, ou nunca pode ver a modificação da parada devido à otimização do compilador.
Devido aos vários problemas nas situações de múltiplas threading acima, a sequência do programa na multi-threading não é mais a ordem de execução e resulta no mecanismo subjacente. A linguagem de programação precisa dar uma garantia aos desenvolvedores. Em termos simples, essa garantia é quando a modificação de um thread será visível para outros threads. Portanto, o idioma Java propõe JavameMoryModel, ou seja, o modelo de memória Java, que requer implementação de acordo com as convenções deste modelo. O Java fornece mecanismos como voláteis, sincronizados e finais para ajudar os desenvolvedores a garantir a correção de programas com vários threads em todas as plataformas de processador.
Antes do JDK1.5, o modelo de memória de Java tinha sérios problemas. Por exemplo, no modelo antigo de memória, um encadeamento pode ver o valor padrão de um campo final após a conclusão do construtor e a gravação do campo volátil pode ser reordenada com a leitura e a gravação do campo não volátil.
Assim, no JDK1.5, um novo modelo de memória foi proposto através do JSR133 para corrigir os problemas anteriores.
Reordenar regras
Volátil e Monitor Lock
| É possível reordenar | A segunda operação | A segunda operação | A segunda operação |
|---|---|---|---|
| A primeira operação | Leitura normal/escrita comum | Read/monitor volátil Enter | Saída volátil de gravação/monitor |
| Leitura normal/escrita comum | Não | ||
| Voaltil Read/Monitor Enter | Não | Não | Não |
| Saída volátil de gravação/monitor | Não | Não |
A leitura normal refere-se ao conjunto de matrizes Getfield, GetStatic e não volátil, e a leitura normal refere-se à matriz das matrizes Putfield, Putstatic e não volátil.
A leitura e a gravação de campos voláteis são Getfield, Getstatic, Putfield, Putstatic, respectivamente.
O Monitorente deve entrar no bloqueio de sincronização ou no método de sincronização, o monitorexista refere -se à saída do bloqueio de sincronização ou do método de sincronização.
Não na tabela acima refere -se a duas operações que não permitem a reordenação. Por exemplo (escrita normal, escrita volátil) refere-se à reordenação de campos não voláteis e à reordenação de gravações de campos voláteis subsequentes. Quando não há não, isso significa que a reordenação é permitida, mas a JVM precisa garantir uma segurança mínima - o valor de leitura é o valor padrão ou escrito por outros threads (operações de leitura e gravação duplas e longas de 64 bits são um caso especial. Quando não há modificação volátil, não é garantida que a leitura e a escrita é atômica, e a camada subjacente pode se separar.
Campo final
Existem duas regras especiais adicionais para o campo final
Nem a gravação do campo final (no construtor) nem a gravação da referência do objeto final do campo podem ser reordenados com as gravações subsequentes dos objetos que mantêm o campo final (fora do construtor). Por exemplo, a seguinte declaração não pode ser reordenada
x.Finalfield = V; ...; sharedref = x;
A primeira carga do campo final não pode ser reordenada com a gravação do objeto que mantém o campo final. Por exemplo, a seguinte declaração não permite a reordenação.
x = sharedref; ...; i = x.Finalfield
Barreira de memória
Todos os processadores suportam certas barreiras ou cercas de memória para controlar a visibilidade da reordenação e dados entre diferentes processadores. Por exemplo, quando a CPU gravará os dados de volta, ele colocará a solicitação da loja no buffer de gravação e aguarda a descarga na memória. Essa solicitação de loja pode ser impedida de ser reordenada com outras solicitações, inserindo a barreira para garantir a visibilidade dos dados. Você pode usar um exemplo de vida para comparar a barreira. Por exemplo, ao tomar um elevador de encostas no metrô, todo mundo entra no elevador em sequência, mas algumas pessoas vão da esquerda, então a ordem ao sair do elevador é diferente. Se uma pessoa carrega uma grande bagagem bloqueada (barreira), as pessoas por trás não podem dar a volta :). Além disso, a barreira aqui e a barreira de gravação usada no GC são conceitos diferentes.
Classificação de barreiras de memória
Quase todos os processadores suportam instruções de barreira de um certo grão grosso, geralmente chamado de cerca (cerca, cerca), que pode garantir que as instruções de carga e armazenamento iniciadas antes da cerca possa ser estritamente em ordem com a carga e a armazenamento após a cerca. Geralmente, será dividido nos quatro tipos de barreiras a seguir, de acordo com seu objetivo.
Barreiras de carga de carga
Carga1; Carga de carga; Carga2;
Verifique se os dados do load1 são carregados antes do carregamento2 e após a carga
Barreiras de Storestore
Store1; Storestore; Store2
Verifique se os dados no Store1 estão visíveis para outros processadores antes da Store2 e depois.
Barreiras de Loadstore
Carga1; Loadstore; Store2
Verifique se os dados do load1 são carregados antes do Store2 e após a descarga de dados
Barreiras de Storeload
Store1; Storeload; Load2
Verifique se os dados no Store1 estão visíveis na frente de outros processadores (como descarga na memória) antes de carregar os dados no load2 e após a carga. A barreira do Storeload impede a leitura de dados antigos, em vez de dados recentemente escritos por outros processadores.
Quase todos os multiprocessadores nos tempos modernos exigem storeload. A sobrecarga do Storeload é geralmente a maior, e o Storeload tem o efeito de três outras barreiras, para que o storeload possa ser usado como uma barreira geral (mas maior sobrecarga).
Portanto, usando a barreira de memória acima, as regras de reordenação na tabela acima podem ser implementadas
| Precisa de barreiras | A segunda operação | A segunda operação | A segunda operação | A segunda operação |
|---|---|---|---|---|
| A primeira operação | Leitura normal | Escrita normal | Read/monitor volátil Enter | Saída volátil de gravação/monitor |
| Leitura normal | Loadstore | |||
| Leitura normal | Storestore | |||
| Voaltil Read/Monitor Enter | Carga de carga | Loadstore | Carga de carga | Loadstore |
| Saída volátil de gravação/monitor | Storeload | Storestore |
Para apoiar as regras dos campos finais, é necessário adicionar uma barreira à gravação final ao final
x.Finalfield = V; Storestore; sharedref = x;
Insira a barreira da memória
Com base nas regras acima, você pode adicionar uma barreira ao processamento de campos voláteis e palavras -chave sincronizadas para atender às regras do modelo de memória.
Insira o Storestore antes da barreira volátil da loja depois que todos os campos finais são escritos, mas insira o Storestore antes que o construtor retorne
Insira a barreira Storeload após uma loja volátil. Insira a carga de carga e a barreira da loja de carga após a carga volátil.
O monitor Enter e as regras voláteis de carga são consistentes e as regras de saída do monitor e lojas voláteis são consistentes.
Acontecer antes
As várias barreiras de memória mencionadas acima ainda são relativamente complexas para os desenvolvedores, para que o JMM possa usar uma série de regras de relações de ordem parcial de acontecer antes de ilustrar. Para garantir que o encadeamento que execute a operação B veja o resultado da operação A (independentemente de A e B serem executados no mesmo encadeamento), o relacionamento antes deve ser atendido entre A e B, caso contrário, a JVM pode reordená -los arbitrariamente.
Acontece que a lista de regras
As regras do HappEndBe antes incluem
Regras de sequência do programa: Se a operação A no programa estiver antes da operação B, a operação A no mesmo encadeamento executará as regras de bloqueio do monitor antes da operação B: a operação de bloqueio na trava do monitor deve ser executada antes da operação de bloqueio no mesmo monitor.
Regras de variáveis voláteis: a operação de gravação da variável volátil deve executar regras de inicialização do encadeamento antes da operação de leitura da variável: a chamada para encadeamento. Antes da Operação B e da Operação B, é executada antes da Operação C, a Operação A é executada antes da Operação C.
A trava de exibição possui a mesma semântica de memória que a trava do monitor, e a variável atômica tem a mesma semântica de memória que a volátil. A aquisição e liberação de bloqueios, as operações de leitura e gravação de variáveis voláteis, satisfazem o relacionamento de ordem inteira, para que a gravação de volátil possa ser realizada antes das leituras voláteis subsequentes.
O acontecimento acima mencionado antes pode ser combinado usando várias regras.
Por exemplo, depois que o thread A entra no bloqueio do monitor, a operação antes de liberar o bloqueio do monitor é baseada nas regras de sequência do programa, e a operação de liberação do monitor aconteceu antes é usada para obter o mesmo monitor bloqueio no encadeamento subsequente B, e a operação na operação em OchaBefore e Thread B.
Resumir
O exposto acima é toda a explicação detalhada do modelo de memória Java JMM neste artigo, espero que seja útil para todos. Se houver alguma falha, deixe uma mensagem para apontá -la. Obrigado amigos pelo seu apoio para este site!