Prefácio
O artigo anterior falou sobre o princípio do CAS, que mencionou a classe atômica*. O mecanismo para implementar operações atômicas depende das características de visibilidade da memória do volátil. Se você ainda não conhece CAS e Atomic*, é recomendável dar uma olhada no que o CAS Spin Lock de que estamos falando.
Três características de concorrência
Primeiro de tudo, se quisermos usar o VOLATILE, ele deve estar em um ambiente de simultaneidade com vários threads. Existem três características importantes no cenário simultâneo sobre as quais falamos: Atomicidade, visibilidade e ordem. Somente quando essas três características forem atendidas, o programa simultâneo pode ser executado corretamente, caso contrário, vários problemas surgirão.
Atomicidade, as classes CAS e Atomic* mencionadas no artigo anterior podem garantir a atomicidade das operações simples. Para algumas operações responsáveis, ele pode ser implementado usando bloqueios sincronizados ou vários.
A visibilidade refere -se a quando vários threads acessam a mesma variável, um encadeamento modifica o valor da variável e outros threads podem ver imediatamente o valor modificado.
Ordem, a ordem de execução do programa é executada na ordem do código e as instruções são proibidas de serem reordenadas. Parece natural que este não seja o caso. A reordenação de instruções é a JVM para otimizar as instruções e melhorar a eficiência da operação do programa e melhorar o paralelismo o máximo possível sem afetar os resultados da execução de um programa de thread único. No entanto, em um ambiente multithread, a ordem de alguns códigos pode causar incorreção lógica.
Volátil implementa dois recursos, visibilidade e ordem. Portanto, em um ambiente multithread, é necessário garantir a função desses dois recursos e a palavra-chave volátil pode ser usada.
Como a visibilidade volátil garante a visibilidade
Quando se trata de visibilidade, você precisa entender o processador e a memória principal do computador. Devido à multi-threading, não importa quantos threads existam, ele será realizado em um processador de computador. Os computadores de hoje são basicamente múltiplos núcleos, e algumas máquinas têm até multiprocessadores. Vamos dar uma olhada no diagrama da estrutura de um multiprocessador:
Esta é uma CPU com dois processadores, um quad-core. Um processador corresponde a um slot físico e vários processadores são conectados através de um barramento QPI. Um processador consiste em vários núcleos e um cache L3 compartilhado com vários núcleos entre os processadores. Um núcleo contém registros, cache L1, cache L2.
Durante a execução do programa, a leitura e a escrita dos dados devem estar envolvidos. Todos sabemos que, embora a velocidade de acesso à memória já seja muito rápida, ainda é muito inferior à velocidade das instruções de execução da CPU. Portanto, no kernel, L1, L2 e L3, são adicionados caches de nível três. Dessa forma, quando o programa está em execução, os dados necessários são copiados pela primeira vez da memória principal para o cache do núcleo e, após a conclusão da operação, eles são gravados na memória principal. A figura a seguir é um diagrama esquemático de dados de acesso à CPU, de registros a cache à memória principal e até discos rígidos, a velocidade está ficando mais mais lenta.
Depois de entender a estrutura da CPU, vamos dar uma olhada no processo específico de execução do programa e fazer uma operação simples de auto-incremento como exemplo.
i = i+1;
Ao executar essa instrução, um thread executado em um núcleo copia o valor de I para o cache onde o núcleo está localizado. Após a conclusão da operação, ela será gravada de volta à memória principal. Em um ambiente multithread, cada encadeamento terá uma memória de trabalho correspondente na área de cache no núcleo em execução, ou seja, cada thread possui sua própria área de cache de trabalho privado para armazenar os dados de réplica necessários para a operação. Então, vamos olhar para o problema de i+1. Supondo que o valor inicial de I seja 0, há dois threads que executam esta declaração ao mesmo tempo, e cada thread precisa de três etapas para executar:
1. Leia o valor I da memória principal para a memória de trabalho do encadeamento, ou seja, a área de cache do kernel correspondente;
2. Calcule o valor de i+1;
3. Escreva o valor do resultado de volta à memória principal;
Depois que os dois threads são executados 10.000 vezes cada, o valor esperado deve ser de 20.000. Infelizmente, o valor de I é sempre menor que 20.000. Uma das razões para esse problema é o problema de consistência do cache. Para este exemplo, uma vez que uma cópia de cache de um encadeamento é modificada, a cópia do cache de outros threads deve ser invalidada imediatamente.
Depois de usar a palavra -chave volátil, os seguintes efeitos serão:
1. Toda vez que a variável é modificada, o cache do processador (memória de trabalho) será gravado de volta à memória principal;
2. Escrever de volta à memória principal de uma memória de trabalho fará com que o cache do processador (memória de trabalho) de outros threads seja inválido.
Como a volátil garante a visibilidade da memória, ele realmente usa o protocolo MESI que garante a consistência do cache pela CPU. Existem muitos conteúdos do protocolo MESI, então não vou explicar aqui. Por favor, verifique você mesmo. Em suma, a palavra -chave volátil é usada. Quando a modificação de um thread na variável volátil será gravada de volta à memória principal imediatamente, fazendo com que a linha de cache de outros threads seja invalidada e outros threads são forçados a usar a variável novamente, ela precisa ser lida na memória principal.
Em seguida, modificamos a variável acima com a variável com volátil e o executamos novamente, cada encadeamento será executado 10.000 vezes. Infelizmente, ainda é menos de 20.000. Por que isso?
A Volatile utiliza o protocolo MESI da CPU para garantir a visibilidade. No entanto, observe que a volátil não garante a atomicidade da operação, porque essa operação de auto-incremento é dividida em três etapas. Suponha que o Thread 1 lê o valor i da memória principal, assumindo que seja 10, e ocorre um bloqueio no momento, mas ainda não foi modificado. Neste momento, o Thread 2 também lê o valor i da memória principal. Neste momento, o valor lido por esses dois threads é o mesmo, ambos 10, e o Thread 2 adiciona 1 a i e o escreve imediatamente de volta à memória principal. Neste momento, de acordo com o protocolo MESI, a linha de cache correspondente à memória de trabalho do Thread 1 será definida como um estado inválido, sim. No entanto, observe que o thread 1 já copiou o valor i da memória principal e agora só leva a operação de adicionar 1 e escrever novamente à memória principal. Ambos os threads adicionam 1 com base em 10 e, em seguida, escrevem de volta à memória principal; portanto, o valor final da memória principal é apenas 11, não o 12 esperado.
Portanto, o uso de volátil pode garantir a visibilidade da memória, mas não pode garantir atomicidade. Se ainda for necessário atomicidade, você pode consultar este artigo anterior.
Como o volátil garante a ordem
O modelo de memória Java tem algum "linha de ordem" inato, ou seja, pode ser garantido sem nenhum meio. Isso geralmente é chamado de Princípio Acontece. Se a ordem de execução de duas operações não puder ser derivada do princípio que acontece, eles não poderão garantir sua ordem e as máquinas virtuais poderão reordená-las à vontade.
A seguir, são apresentados 8 princípios de acontecer, extraídos da "compreensão profunda das máquinas virtuais de Java".
Aqui falaremos principalmente sobre as regras da palavra -chave volátil e daremos um exemplo de checagem dupla no famoso padrão de singleton:
classe singleton {private volátil estático singleton instance = null; private singleton () {} public static singleton getInstance () {if (instance == null) {// etapa 1 sincronizada (singleton.class) {if (instance == null) // etapa 2 instância = new singleton (); // Etapa 3}} Retornar a instância; }}Se a instância não for modificada com volátil, quais resultados podem ser produzidos? Suponha que haja dois threads chamando o método getInstance (). O thread 1 executa a etapa1 e descobre que a instância é nula e, em seguida, bloqueia a classe singleton de maneira síncrona. Em seguida, determina se a instância é nula novamente e descobre que ainda é nula e, em seguida, executa a Etapa 3 e inicia a instanciando singleton. Durante o processo de instanciação, o Thread 2 vai para a etapa 1 e pode achar que a instância não está vazia, mas neste momento a instância pode não ser totalmente inicializada.
O que isso significa? O objeto é inicializado em três etapas e é representado pelo seguinte pseudo-código:
Memory = alocate (); // 1. Alocar o espaço de memória do objeto ctorInstance (memória); // 2. Inicialize a instância do objeto = memória; // 3. Defina o espaço de memória do objeto apontando para o objeto
Como a etapa 2 e a etapa 3 precisam depender da etapa 1 e a etapa 2 e 3 não têm uma dependência, é possível que essas duas declarações sejam submetidas a relembrogem de instruções, isto é, ou é possível que a etapa 3 seja executada antes da etapa 2. Nesse caso, a etapa 3 é executada, mas a etapa 2 ainda não foi executada, a instância ainda não foi inicializada. Agora, o Thread 2 juiz que a instância não é nula, por isso retorna diretamente a instância da instância. No entanto, neste momento, a instância é na verdade um objeto incompleto; portanto, haverá problemas ao usá -lo.
O uso da palavra-chave volátil significa usar o princípio de "escrever uma variável modificada por volátil, acontece antes de ler a variável em qualquer tempo subsequente" corresponde ao processo de inicialização acima. As etapas 2 e 3 são instâncias de escrita, portanto, elas devem ocorrer posteriormente ao ler as instâncias, ou seja, não haverá possibilidade de retornar uma instância que não seja completamente inicializada.
A JVM subjacente é feita através de algo chamado "barreira de memória". A barreira da memória, também conhecida como cerca de memória, é um conjunto de instruções do processador usadas para implementar restrições seqüenciais nas operações de memória.
afinal
Através da palavra -chave volátil, aprendemos sobre a visibilidade e a ordem na programação simultânea, o que é obviamente apenas um entendimento simples. Para um entendimento mais profundo, você deve confiar em seus colegas de classe para estudá -lo sozinho.
Artigos relacionados
Quais são as fechaduras de spin cas que estamos falando
Resumir
O acima é o conteúdo inteiro deste artigo. Espero que o conteúdo deste artigo tenha certo valor de referência para o estudo ou trabalho de todos. Se você tiver alguma dúvida, pode deixar uma mensagem para se comunicar. Obrigado pelo seu apoio ao wulin.com.