Muitos materiais on -line descrevem o modelo de memória Java, que introduzirá que existe uma memória principal, e cada thread de trabalhadores tem sua própria memória de trabalho. Haverá uma peça de dados na memória principal e uma peça na memória de trabalho. Haverá várias operações atômicas entre a memória de trabalho e a memória principal para sincronizar.
A imagem a seguir é deste blog
No entanto, devido à evolução contínua da versão Java, o modelo de memória também mudou. Este artigo fala apenas sobre alguns recursos do modelo de memória Java. Seja um novo modelo de memória ou um modelo de memória antigo, ele ficará mais claro depois de entender esses recursos.
1. Atomicidade
Atomicidade significa que uma operação é ininterrupta. Mesmo quando vários threads são executados juntos, uma vez que uma operação é iniciada, ela não será perturbada por outros threads.
Acredita -se geralmente que as instruções da CPU sejam operações atômicas, mas o código que escrevemos não é necessariamente operações atômicas.
Por exemplo, i ++. Esta operação não é uma operação atômica, é basicamente dividida em 3 operações, leia i, executar +1 e atribuir valor a i.
Suponha que haja dois threads. Quando o primeiro thread lê i = 1, a operação +1 ainda não foi realizada e mude para o segundo thread. Neste momento, o segundo thread também lê i = 1. Em seguida, os dois threads executam operações subsequentes +1 e, em seguida, atribuem os valores de volta, eu não é 3, mas 2. Obviamente, há inconsistência nos dados.
Por exemplo, ler um valor longo de 64 bits em uma JVM de 32 bits não é uma operação atômica. Obviamente, a JVM de 32 bits lê números inteiros de 32 bits como uma operação atômica.
2. Ordem
Durante a concorrência, a execução do programa pode estar fora de ordem.
Quando um computador executa o código, ele não é necessariamente executado na ordem do programa.
classe ordeRexample {int a = 0; bandeira booleana = false; public void writer () {a = 1; bandeira = true; } public void reader () {if (flag) {int i = a +1; }}} Por exemplo, no código acima, dois métodos são chamados por dois threads, respectivamente. De acordo com o bom senso, o tópico de escrita deve primeiro executar a = 1 e depois executar sinalizador = true. Quando o tópico de leitura está lendo, i = 2;
Mas porque a = 1 e sinalizador = verdadeiro, não há correlação lógica. Portanto, é possível reverter a ordem de execução e é possível executar sinalizador = true primeiro e depois a = 1. Neste momento, quando o sinalizador = true, mude para o thread de leitura. No momento, a = 1 ainda não foi executado, o thread de leitura será i = 1.
Claro que isso não é absoluto. É possível que haja fora de ordem e não aconteça.
Então, por que está fora de ordem? Isso começa com a instrução da CPU. Depois que o código em Java é compilado, ele é finalmente convertido em código de montagem.
A execução de uma instrução pode ser dividida em muitas etapas. Supondo que a instrução da CPU seja dividida nas etapas a seguir
Suponha que há duas instruções aqui
De um modo geral, pensaremos que as instruções são executadas em série, primeiro executam a instrução 1 e depois executam a instrução 2. Supondo que cada etapa requer 1 período de tempo da CPU, a execução dessas duas instruções requer 10 períodos de tempo da CPU, o que é muito ineficiente para fazê -lo. De fato, as instruções são executadas em paralelo. Obviamente, quando a primeira instrução é executada se, a segunda instrução não puder executar, porque os registros de instruções e similares não puderem ser ocupados ao mesmo tempo. Assim, como mostrado na figura acima, as duas instruções são executadas em paralelo de uma maneira relativamente escalonada. Quando a instrução 1 executa o ID, a instrução 2 executa o if. Dessa forma, duas instruções foram executadas em apenas 6 períodos de tempo da CPU, o que foi relativamente eficiente.
De acordo com essa ideia, vamos dar uma olhada em como a instrução A = B+C é executada.
Como mostrado na figura, existe uma operação inativa (x) durante a operação de adição, porque quando você deseja adicionar B e C, quando a operação X de Add's na figura, C não lê a partir da memória (C apenas leia da memória quando a operação do MEM é concluída. É possível que o R1 e o R1 e o R1 e o R1 e o R1 seja adicionado? Hardware, portanto, não há necessidade de esperar a execução do WB antes da execução do ADD). Portanto, haverá um tempo ocioso (x) para adicionar operação. Na operação SW, como a instrução EX não pode ser realizada simultaneamente com a instrução ADD EX, haverá um tempo ocioso (x).
Em seguida, vamos dar um exemplo um pouco mais complicado
a = b+c
d = ef
As instruções correspondentes são as seguintes
O motivo é semelhante ao acima, então não vou analisá -lo aqui. Descobrimos que há muito X aqui e há muitos ciclos de tempo desperdiçados, e o desempenho também é afetado. Existe uma maneira de reduzir o número de Xs?
Esperamos usar algumas operações para preencher o tempo livre de X, porque a ADD tem dependência de dados com as instruções acima e esperamos usar algumas instruções sem dependência de dados para preencher o tempo livre gerado pela dependência de dados.
Mudamos a ordem das instruções
Depois de alterar a ordem das instruções, X é eliminado. O período geral de tempo de execução também diminuiu.
A reordenação de instruções pode tornar o pipeline mais suave
Obviamente, o princípio do rearranjo de instruções é que ele não pode destruir a semântica do programa serial. Por exemplo, a = 1, b = a+1, essas instruções não serão reorganizadas porque o resultado serial do rearranjo é diferente do original.
O rearranjo de instruções é apenas uma maneira de otimizar o compilador ou a CPU, e essa otimização causou problemas com o programa no início deste capítulo.
Como resolvê -lo? Use a palavra -chave volátil, esta série subsequente será introduzida.
3. Visibilidade
A visibilidade refere -se a se outros threads podem saber imediatamente a modificação quando um encadeamento modifica o valor de uma variável compartilhada.
Questões de visibilidade podem surgir em vários links. Por exemplo, a reordenação de instruções mencionada também causará problemas de visibilidade e, além disso, a otimização do compilador ou a otimização de certos hardware também causará problemas de visibilidade.
Por exemplo, um encadeamento otimiza um valor compartilhado na memória, enquanto outro thread otimiza o valor compartilhado no cache. Ao modificar o valor na memória, o valor em cache não conhece a modificação.
Por exemplo, algumas otimizações de hardware, quando um programa grava várias vezes no mesmo endereço, ele acha desnecessário e mantém apenas a última gravação; portanto, os dados gravados antes serão invisíveis em outros threads.
Em suma, a maioria dos problemas com visibilidade decorre da otimização.
Em seguida, vejamos um problema de visibilidade decorrente do nível Java Virtual Machine
O problema vem de um blog
pacote edu.hushi.jvm; /** * * @author -10 * */public class visibilitytest estende thread {parada booleana privada; public void run () {int i = 0; while (! Stop) {i ++; } System.out.println ("Finalize Loop, i =" + i); } public void stopit () {stop = true; } public boolean getStop () {return Stop; } public static void main (string [] args) lança Exceção {visibilidadeTest V = new VisibilityTest (); v.start (); Thread.sleep (1000); v.stopit (); Thread.sleep (2000); System.out.println ("Finalize Main"); System.out.println (v.getStop ()); }} O código é muito simples. O thread V mantém I ++ no loop while até que o encadeamento principal chama o método de parada, alterando o valor da variável de parada no encadeamento V para parar o loop.
Os problemas ocorrem quando o código aparentemente simples é executado. Este programa pode impedir que os threads realizem operações de auto-incremento no modo cliente, mas no modo servidor, será um loop infinito primeiro. (Mais otimização da JVM no modo de servidor)
A maioria dos sistemas de 64 bits é o modo de servidor e é executada no modo de servidor:
termine principal
verdadeiro
Somente essas duas frases serão impressas, mas o loop de acabamento não será impresso. Mas você pode descobrir que o valor de parada já é verdadeiro.
O autor deste blog usa ferramentas para restaurar o programa para o código de montagem
Apenas uma parte do código de montagem é interceptada aqui e a parte vermelha é a parte do loop. Pode -se ver claramente que apenas 0x0193bf9d é a verificação de parada, enquanto a parte vermelha não aceita o valor de parada, portanto um loop infinito é realizado.
Este é o resultado da otimização da JVM. Como evitá -lo? Como a reordenação da diretiva, use a palavra -chave volátil.
Se for adicionado volátil, restaure -o ao código de montagem e você descobrirá que cada loop receberá o valor de parada.
Em seguida, vamos dar uma olhada em alguns exemplos na "especificação de idioma java"
A figura acima mostra que a reordenação de instruções levará a resultados diferentes.
A razão pela qual r5 = r2 é feita na figura acima é que r2 = r1.x, r5 = r1.x e é otimizado diretamente para r5 = r2 no tempo de compilação. No final, os resultados são diferentes.
4. Aconteça antes
5. O conceito de segurança de threads
Refere-se ao fato de que, quando uma certa função ou biblioteca de funções é chamada em um ambiente multithread, ela pode processar corretamente as variáveis locais de cada encadeamento e permitir que as funções do programa sejam concluídas corretamente.
Por exemplo, o exemplo i ++ mencionado no início
Isso levará a thread inseguro.
Para detalhes sobre a segurança do tópico, consulte este blog que escrevi antes ou siga a série subsequente e você também falará sobre conteúdo relacionado.