Muitos amigos podem ter ouvido falar da palavra -chave volátil e podem ter usado. Antes do Java 5, era uma palavra -chave controversa, pois usá -la em programas geralmente resultou em resultados inesperados. Somente depois que o Java 5 a palavra -chave volátil recuperou sua vitalidade.
Embora a palavra -chave volátil seja literalmente simples de entender, não é fácil usá -la bem. Como a palavra -chave volátil está relacionada ao modelo de memória de Java, antes de contar à chave volátil, primeiro entendemos os conceitos e o conhecimento relacionados ao modelo de memória e depois analisamos o princípio da implementação da palavra -chave volátil e, finalmente, fornecemos vários cenários de uso da palavra -chave volátil.
Aqui está o esboço do diretório deste artigo:
1. Conceitos relacionados de modelos de memória
Como todos sabemos, quando um computador executa um programa, cada instrução é executada na CPU e, durante a execução da instrução, envolverá inevitavelmente a leitura e a escrita dos dados. Como os dados temporários durante a operação do programa são armazenados na memória principal (memória física), há um problema no momento. Como a velocidade de execução da CPU é muito rápida, o processo de leitura de dados da memória e a gravação de dados para a memória é muito mais lento que a execução de instruções da CPU. Portanto, se a operação de dados for realizada através da interação com a memória a qualquer momento, a velocidade da execução de instruções será bastante reduzida. Portanto, há um cache na CPU.
Ou seja, quando o programa estiver em execução, ele copiará os dados exigidos pela operação da memória principal para o cache da CPU. Então, quando a CPU executa cálculos, ela pode ler diretamente os dados de seu cache e escrever dados para ele. Após a conclusão da operação, os dados no cache serão liberados na memória principal. Vamos dar um exemplo simples, como o seguinte código:
i = i + 1;
Quando o thread executar essa instrução, ele primeiro lerá o valor de I da memória principal e copiará uma cópia no cache e, em seguida, a CPU executará a instrução para adicionar 1 a i e, em seguida, escrever os dados no cache e, finalmente, atualizar o valor mais recente de i no cache à memória principal.
Não há problema com esse código em execução em um único thread, mas haverá problemas ao executar em um multi-thread. Nas CPUs com vários núcleos, cada encadeamento pode ser executado em uma CPU diferente, portanto, cada encadeamento tem seu próprio cache ao ser executado (para CPUs de núcleo único, esse problema realmente ocorrerá, mas é executado separadamente na forma de agendamento de encadeamentos). Neste artigo, tomamos a CPU com vários núcleos como exemplo.
Por exemplo, dois threads executam esse código ao mesmo tempo. Se o valor de I for 0 no início, esperamos que o valor de eu me torne 2 após a execução dos dois threads. Mas será esse o caso?
Pode haver uma das seguintes situações: No início, dois threads leem o valor de I e o armazenam no cache de suas respectivas CPUs e, em seguida, Thread 1 executa uma operação de adicionar 1 e depois grava o valor mais recente de I na memória. Nesse momento, o valor de I no cache do thread 2 ainda é 0. Depois de executar a operação 1, o valor de I é 1 e, em seguida, o Thread 2 grava o valor de I na memória.
O valor do resultado final I é 1, não 2. Este é o famoso problema de consistência do cache. Essa variável acessada por vários threads é geralmente chamada de variável compartilhada.
Ou seja, se uma variável for armazenada em cache em várias CPUs (geralmente ocorre apenas durante a programação multithreading), pode haver um problema de inconsistência de cache.
Para resolver o problema de inconsistência do cache, geralmente existem duas soluções:
1) adicionando bloqueio# bloqueio ao ônibus
2) através do protocolo de coerência de cache
Esses dois métodos são fornecidos no nível do hardware.
Nas CPUs iniciais, o problema da inconsistência do cache foi resolvido adicionando bloqueios de bloqueio ao barramento. Como a comunicação entre a CPU e outros componentes é realizada através do barramento, se o barramento for adicionado com um bloqueio de bloqueio, isso significa que outras CPUs estão impedidas de acessar outros componentes (como memória), para que apenas uma CPU possa usar a memória dessa variável. Por exemplo, no exemplo acima, se um thread estiver executando i = i +1, e se o sinal de bloqueio LCOK# for enviado no barramento durante a execução deste código, somente depois de aguardar o código totalmente executado, outras CPUs podem ler a variável da memória em que a variável I está localizada e, em seguida, executar as operações correspondentes. Isso resolve o problema da inconsistência do cache.
Mas o método acima terá um problema, porque outras CPUs não podem acessar a memória durante a trava do barramento, resultando em ineficiência.
Portanto, surge um protocolo de consistência do cache. O mais famoso é o protocolo MESI da Intel, que garante que a cópia das variáveis compartilhadas usadas em cada cache seja consistente. Sua idéia principal é: quando a CPU grava dados, se descobrir que a variável que é operada é uma variável compartilhada, ou seja, existe uma cópia da variável em outras CPUs, ela sinalizará outras CPUs para definir a linha de cache da variável para um estado inválido. Portanto, quando outras CPUs precisam ler essa variável e descobrir que a linha de cache que armazena em cache a variável em seu cache é inválida, ela reler da memória.
2. Três conceitos em programação simultânea
Na programação simultânea, geralmente encontramos os três problemas a seguir: problema de atomicidade, problema de visibilidade e problema ordenado. Vamos dar uma olhada nesses três conceitos primeiro:
1. Atomicidade
Atomicidade: ou seja, uma operação ou várias operações são executadas e o processo de execução não será interrompido por nenhum fator ou não será executado.
Um exemplo muito clássico é o problema de transferência de contas bancárias:
Por exemplo, se você transferir 1.000 yuan da conta A para a conta B, ele inevitavelmente incluirá 2 operações: subtrair 1.000 yuan da conta A e adicionar 1.000 yuan à conta B.
Imagine quais consequências serão causadas se essas duas operações não forem atômicas. Se 1.000 yuan forem subtraídos da conta A, a operação será repentinamente encerrada. Então, 500 yuan foram retirados de B e, depois de retirar 500 yuan, a operação de adicionar 1.000 yuan à conta B. isso levará ao fato de que, embora a conta A tenha menos 1.000 yuan, a conta B não recebeu os 1.000 yuan transferidos.
Portanto, essas duas operações devem ser atômicas para garantir que não haja problemas inesperados.
Quais são os resultados que serão refletidos na programação simultânea?
Para dar o exemplo mais simples, pense no que aconteceria se o processo de atribuição de uma variável de 32 bits não for atômico?
i = 9;
Se um thread executar esta declaração, assumirei que a atribuição de uma variável de 32 bits inclui dois processos: atribuição de uma menor atribuição de 16 bits e uma atribuição de 16 bits mais altos.
Em seguida, pode ocorrer uma situação: quando o valor baixo de 16 bits é escrito, ela é de repente interrompida e, neste momento, outro thread lê o valor de I, então o que é lido é os dados errados.
2. Visibilidade
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.
Para um exemplo simples, consulte o seguinte código:
// O código executado pelo encadeamento 1 é int i = 0; i = 10; // O código executado pelo Thread 2 é J = i;
Se o thread de execução 1 for CPU1 e o thread de execução 2 for CPU2. A partir da análise acima, podemos ver que, quando o encadeamento 1 executa a frase i = 10, o valor inicial de I será carregado no cache do CPU1 e depois atribuiu um valor de 10. Então o valor de I no cache do CPU1 se torna 10, mas não é escrito imediatamente para a memória principal.
Neste momento, o Thread 2 executa J = I, e primeiro irá para a memória principal para ler o valor de i e carregá -lo no cache do CPU2. Observe que o valor de I na memória ainda é 0, portanto, o valor de J será 0, não 10.
Esta é a questão da visibilidade. Após o thread 1 modifica a variável i, o thread 2 não vê imediatamente o valor modificado pelo encadeamento 1.
3. Ordem
Ordem: isto é, a ordem de execução dos programas é executada na ordem do código. Para um exemplo simples, consulte o seguinte código:
int i = 0; bandeira booleana = false; i = 1; // Declaração 1 sinalizador = true; // Declaração 2
O código acima define uma variável do tipo int, uma variável do tipo booleano e, em seguida, atribui valores às duas variáveis, respectivamente. Do ponto de vista da sequência de código, a declaração 1 é antes da declaração 2. Portanto, quando a JVM realmente executar esse código, garantirá que a declaração 1 seja executada antes da declaração 2? Não necessariamente, por quê? A reordenação de instruções pode ocorrer aqui.
Vamos explicar o que é reordenação de instruções. De um modo geral, para melhorar a eficiência da operação do programa, o processador pode otimizar o código de entrada. Ele não garante que a ordem de execução de cada instrução no programa seja consistente com o pedido no código, mas garantirá que o resultado final da execução do programa e o resultado da sequência de execução do código sejam consistentes.
Por exemplo, no código acima, que executa a declaração 1 e a declaração 2 primeiro não tem efeito no resultado final do programa, é possível que, durante o processo de execução, a declaração 2 seja executada primeiro e a declaração 1 seja executada posteriormente.
Mas esteja ciente de que, embora o processador reordenasse as instruções, ele garantirá que o resultado final do programa seja o mesmo que a sequência de execução do código. Então, o que garante que é? Vamos dar uma olhada no exemplo a seguir:
int a = 10; // declaração 1int r = 2; // declaração 2a = a + 3; // Declaração 3r = a*a; // Declaração 4
Este código possui 4 instruções, portanto, uma possível ordem de execução é:
Então é possível ser a Ordem de Execução: Declaração 2 Declaração 4 Declaração 3 3
Não é possível porque o processador considerará a dependência de dados entre as instruções ao reordenar. Se uma instrução de instrução 2 deve usar o resultado da instrução 1, o processador garantirá que a Instrução 1 seja executada antes da Instrução 2.
Embora a reordenação não afete os resultados da execução do programa em um único thread, e o multithreading? Vamos ver um exemplo abaixo:
// Thread 1: context = loadContext (); // estado 1Initado = true; // Estado 2 // Thread 2: while (! Inited) {sleep ()} DosomethingWithConfig (contexto);No código acima, como as declarações 1 e 2 não têm dependências de dados, elas podem ser reordenadas. Se ocorrer a reordenação, a declaração 2 é executada pela primeira vez durante a execução do Thread 1, e este é o Thread 2, pensará que o trabalho de inicialização foi concluído e, em seguida, ele sairá do loop while para executar o método DosomethingWithConfig (contexto). No momento, o contexto não é inicializado, o que causará um erro de programa.
Como pode ser visto no exposto, a reordenação de instruções não afetará a execução de um único encadeamento, mas afetará a correção da execução simultânea dos threads.
Em outras palavras, para executar os programas concorrentes corretamente, a atomicidade, a visibilidade e a ordem devem ser garantidas. Enquanto não estiver garantido, isso pode fazer com que o programa seja executado incorretamente.
3.Java Modelo de memória
Conversei sobre alguns problemas que podem surgir em modelos de memória e programação simultânea. Vamos dar uma olhada no modelo de memória Java e estudar o que garante que o modelo de memória Java forneça para nós e quais métodos e mecanismos são fornecidos em Java para garantir a correção da execução do programa ao realizar programação multidreada.
Na especificação Java Virtual Machine, é tentado definir um modelo de memória Java (JMM) para bloquear as diferenças de acesso à memória entre várias plataformas de hardware e sistemas operacionais, de modo a permitir que os programas Java obtenham efeitos consistentes de acesso à memória em várias plataformas. Então, o que o modelo de memória Java estipula? Ele define as regras de acesso para variáveis em um programa. Para ser de maneira mais ampla, define a ordem de execução do programa. Observe que, para obter melhor desempenho de execução, o modelo de memória Java não restringe o mecanismo de execução de usar os registros ou caches do processador para melhorar a velocidade de execução de instruções, nem restringe o compilador a reordenar as instruções. Em outras palavras, no modelo de memória Java, também haverá problemas de consistência do cache e problemas de reordenação de instruções.
O modelo de memória Java estipula que todas as variáveis estão na memória principal (semelhante à memória física mencionada acima) e cada encadeamento tem sua própria memória de trabalho (semelhante ao cache anterior). Todas as operações de um encadeamento em uma variável devem ser executadas na memória de trabalho e não podem operar diretamente na memória principal. E cada thread não pode acessar a memória de trabalho de outros threads.
Para dar um exemplo simples: em Java, execute a seguinte declaração:
i = 10;
O thread de execução deve primeiro atribuir a linha de cache onde a variável I está localizada em seu próprio thread de trabalho e depois escrevê -lo na memória principal. Em vez de escrever o valor 10 diretamente na memória principal.
Então, o que garante a própria linguagem Java fornece atomicidade, visibilidade e ordem?
1. Atomicidade
Em Java, as operações de leitura e atribuição de variáveis dos tipos de dados básicas são operações atômicas, ou seja, essas operações não podem ser interrompidas e executadas ou não.
Embora a frase acima pareça simples, não é tão fácil de entender. Veja o seguinte exemplo i:
Analise quais das seguintes operações são operações atômicas:
x = 10; // declaração 1y = x; // Declaração 2x ++; // declaração 3x = x + 1; // Declaração 4
À primeira vista, alguns amigos podem dizer que as operações nas quatro declarações acima são todas operações atômicas. De fato, apenas a declaração 1 é uma operação atômica e nenhuma das outras três declarações são operações atômicas.
A declaração 1 atribui diretamente o valor 10 a x, o que significa que o thread executa esta instrução e grava o valor 10 diretamente na memória de trabalho.
A declaração 2 realmente contém 2 operações. Primeiro, ele precisa ler o valor de x e depois escrever o valor de x para a memória de trabalho. Embora as duas operações de ler o valor de X e escrever o valor de x para a memória de trabalho sejam operações atômicas, elas não são operações atômicas juntas.
Da mesma forma, x ++ e x = x+1 incluem 3 operações: leia o valor de x, execute a operação de adicionar 1 e gravar o novo valor.
Portanto, apenas a operação da declaração 1 nas quatro declarações acima é atômica.
Em outras palavras, apenas leitura e atribuição simples (e o número deve ser atribuído a uma variável, e a atribuição mútua entre variáveis não é uma operação atômica) é uma operação atômica.
No entanto, há uma coisa a ser observada aqui: na plataforma de 32 bits, a leitura e a atribuição de dados de 64 bits precisam ser concluídas em duas operações e sua atomicidade não pode ser garantida. No entanto, parece que, no último JDK, a JVM garantiu que a leitura e a atribuição de dados de 64 bits também sejam operação atômica.
Do exposto, pode -se observar que o modelo de memória Java garante apenas que leituras e atribuições básicas sejam operações atômicas. Se você deseja alcançar a atomicidade de uma gama maior de operações, ela pode ser alcançada por meio de sincronizado e bloqueio. Como sincronizado e bloqueio podem garantir que apenas um encadeamento execute o bloco de código a qualquer momento, naturalmente não haverá problema de atomicidade, garantindo a atomicidade.
2. Visibilidade
Para visibilidade, o Java fornece a palavra -chave volátil para garantir a visibilidade.
Quando uma variável compartilhada é modificada por volátil, garante que o valor modificado seja atualizado para a memória principal imediatamente e, quando outros threads precisarem lê -lo, ele lerá o novo valor na memória.
No entanto, variáveis compartilhadas comuns não podem garantir visibilidade, porque é incerto quando a variável compartilhada normal é gravada na memória principal após a modificação. Quando outros threads o leem, o valor antigo original ainda pode estar na memória; portanto, a visibilidade não pode ser garantida.
Além disso, sincronizado e bloqueio também podem garantir a visibilidade. O sincronizado e o bloqueio podem garantir que apenas um thread adquira o bloqueio ao mesmo tempo e execute o código de sincronização. Antes de liberar o bloqueio, a modificação da variável será atualizada na memória principal. Portanto, a visibilidade pode ser garantida.
3. Ordem
No modelo de memória Java, os compiladores e os processadores podem reordenar as instruções, mas o processo de reordenação não afetará a execução de programas de thread único, mas afetará a correção da execução simultânea multitreada.
Em Java, uma certa "linha de ordem" pode ser garantida através da palavra -chave volátil (o princípio específico é explicado na próxima seção). Além disso, sincronizado e bloqueio podem ser usados para garantir o pedido. Obviamente, sincronizado e bloqueio garantem que exista um thread que execute o código de sincronização a cada momento, o que é equivalente a permitir que os threads executem o código de sincronização na sequência, que naturalmente garante a ordem.
Além disso, o modelo de memória Java possui algum "linha de ordem" inato, ou seja, pode ser garantido sem nenhum meio, que geralmente é chamado de princípio que acontece antes. 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.
Vamos apresentar o princípio de acontecer antes (princípio de ocorrência prioritária):
Esses 8 princípios são extraídos da "compreensão profunda das máquinas virtuais Java".
Entre essas 8 regras, as 4 primeiras regras são mais importantes, enquanto as 4 últimas regras são óbvias.
Vamos explicar as 4 primeiras regras abaixo:
Para regras de ordem do programa, meu entendimento é que a execução de um código de programa parece ser encomendada em um único thread. Observe que, embora essa regra mencione que "a operação escrita na frente ocorre primeiro na operação escrita no fundo", essa deve ser a ordem em que o programa parece ser executado na sequência de código, porque a máquina virtual pode reordenar o código do programa instruído. Embora a reordenação seja realizada, o resultado final da execução é consistente com a execução seqüencial do programa e reordenará apenas as instruções que não possuem dependências de dados. Portanto, em um único thread, a execução do programa parece ser executada de maneira ordenada, que deve ser entendida com cuidado. De fato, essa regra é usada para garantir a correção dos resultados da execução do programa em um único thread, mas não pode garantir a correção do programa de maneira multitreada.
A segunda regra também é mais fácil de entender, ou seja, se o mesmo bloqueio estiver em um estado bloqueado, ela deve ser liberada antes que a operação de bloqueio possa continuar.
A terceira regra é uma regra relativamente importante e também é o que será discutido posteriormente. Intuitivamente, se um thread gravar uma variável primeiro e depois um thread lê, a operação de gravação ocorrerá definitivamente primeiro na operação de leitura.
A quarta regra realmente reflete que o princípio acontece antes é transitivo.
4. Análise aprofundada de palavras-chave voláteis
Eu já falei sobre muitas coisas antes, mas elas estão realmente abrindo caminho para dizer a palavra -chave volátil, então vamos ao tópico.
1. Semântica de duas camadas de palavras-chave voláteis
Uma vez que uma variável compartilhada (variáveis de membro da classe, as variáveis de membro estático de classe) é modificado por volátil, ela possui duas camadas de semântica:
1) Garanta a visibilidade de diferentes encadeamentos ao operar essa variável, ou seja, um thread modifica o valor de uma determinada variável e esse novo valor é imediatamente visível para outros threads.
2) É proibido reordenar as instruções.
Vejamos primeiro um pedaço de código. Se o thread 1 for executado primeiro e o thread 2 for executado posteriormente:
// Thread 1Boolean Stop = false; while (! Stop) {Dosomething ();} // Thread 2Stop = true;Este código é uma parte de código muito típica, e muitas pessoas podem usar esse método de marcação ao interromper threads. Mas, de fato, esse código será executado completamente corretamente? O tópico será interrompido? Não necessariamente. Talvez na maioria das vezes, esse código possa interromper os threads, mas também pode fazer com que o thread não seja interrompido (embora essa possibilidade seja muito pequena, uma vez que isso acontecer, isso causará um loop morto).
Vamos explicar por que esse código pode fazer com que o thread não interrompa. Conforme explicado anteriormente, cada encadeamento tem sua própria memória de trabalho durante a operação; portanto, quando o Thread 1 estiver em execução, ele copiará o valor da variável de parada e o colocará em sua própria memória de trabalho.
Então, quando o thread 2 altera o valor da variável de parada, mas não teve tempo de escrevê -lo na memória principal, o Thread 2 vai fazer outras coisas, então o thread 1 não sabe sobre as alterações do Thread 2 na variável de parada, para que continue a fazer o loop.
Mas depois de modificar com volátil, torna -se diferente:
Primeiro: o uso da palavra -chave volátil forçará o valor modificado a ser gravado na memória principal imediatamente;
Segundo: se você usar a palavra -chave volátil, quando o Thread 2 a modificar, a linha de cache da variável de cache parou na memória de trabalho do Thread 1 será inválida (se for refletida na camada de hardware, a linha de cache correspondente no cache L1 ou L2 da CPU é inválida);
Terceiro: Como a linha de cache da variável de cache Stop na memória de trabalho do Thread 1 é inválida, o Thread 1 o lerá na memória principal quando lê o valor da parada variável novamente.
Então, quando o Thread 2 modifica o valor de parada (é claro, existem 2 operações aqui, modificando o valor na memória de trabalho do Thread 2 e, em seguida, escrevendo o valor modificado na memória), a linha de cache da variável de cache parada na memória de trabalho do Thread 1 será inválida. Quando o Thread 1 lê, ele descobre que sua linha de cache é inválida. Ele aguardará a atualização do endereço de memória principal correspondente da linha de cache e leia o valor mais recente na memória principal correspondente.
Então, o que o Thread 1 lê é o valor correto mais recente.
2. O volátil garante atomicidade?
Pelo acima, sabemos que a palavra -chave volátil garante a visibilidade das operações, mas pode garantir que as operações nas variáveis sejam atômicas?
Vamos ver um exemplo abaixo:
Public class Test {public volatile int inc = 0; public void aument () {Inc ++; } public static void main (string [] args) {final teste de teste = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.começar(); } while (thread.activeCount ()> 1) // Verifique se os threads anteriores foram concluídos thread.yield (); System.out.println (test.inc); }}Pense em qual é o resultado da saída deste programa? Talvez alguns amigos pensem que são 10.000. Mas, de fato, a execução descobrirá que os resultados de cada execução são inconsistentes e é um número inferior a 10.000.
Alguns amigos podem ter perguntas, está errado. O exposto acima é executar operação de auto-incremento na variável inc. Como a volátil garante a visibilidade, após o auto-incremento do incorto em cada encadeamento, o valor modificado pode ser visto em outros threads. Portanto, 10 threads executaram 1000 operações, respectivamente, portanto, o valor final do INC deve ser de 1000*10 = 10000.
Há um mal -entendido aqui. A palavra -chave volátil pode garantir a visibilidade, mas o programa acima está errado porque não pode garantir atomicidade. A visibilidade só pode garantir que o valor mais recente seja lido todas as vezes, mas a volátil não pode garantir a atomicidade da operação de variáveis.
Como mencionado anteriormente, a operação de incremento automático não é atômica. Inclui a leitura do valor original de uma variável, a execução de uma operação adicional e a gravação da memória de trabalho. Ou seja, as três sub-operações da operação de auto-incremento podem ser realizadas separadamente, o que pode levar à seguinte situação:
Se o valor da Variable Inc em um determinado tempo for 10,
O thread 1 executa operação de auto-incremento na variável. O thread 1 lê o valor original da variável inc e depois o thread 1 é bloqueado;
Em seguida, o Thread 2 executa operação de auto-incremento na variável e o Thread 2 também lê o valor original da variável inc. Como o Thread 1 executa apenas uma operação de leitura na variável Inc e não modifica a variável, ela não causará a linha de cache da variável de cache Cache Cache no Thread 2 para ser inválida. Portanto, o thread 2 irá diretamente para a memória principal para ler o valor de inc. Quando se vê que o valor do INC é 10, executa uma operação de adicionar 1 e grava 11 na memória de trabalho e finalmente o escreve na memória principal.
Em seguida, o thread 1 executa a operação de adição. Como o valor do INC foi lido, observe que o valor do INC no thread 1 ainda é 10 no momento, portanto, depois que o thread 1 adiciona Inc, o valor do INC é 11, então escreve 11 para trabalhar na memória e, finalmente, o grava na memória principal.
Depois que os dois threads executam uma operação de auto-incremento, o INC aumenta apenas em 1.
Tendo explicado isso, alguns amigos podem ter perguntas, está errado. Não é garantido que uma variável invalidará a linha de cache ao modificar a variável volátil? Em seguida, outros threads lerão o novo valor. Sim, isso está correto. Esta é a regra variável volátil na regra que acontece antes, mas deve-se notar que, se o Thread 1 lê a variável e for bloqueado, o valor do INC não será modificado. Então, embora o Volátil possa garantir que o Thread 2 leia o valor do Variable Inc da memória, o Thread 1 não o modificou; portanto, o Thread 2 não verá o valor modificado.
A causa raiz é que a operação de auto -increment não é uma operação atômica e volátil não pode garantir que qualquer operação em variáveis seja atômica.
Altere o código acima para qualquer um dos seguintes pode alcançar o efeito:
Use sincronizado:
Public class Test {public int Inc = 0; public sincronizado void aument () {Inc ++; } public static void main (string [] args) {final teste de teste = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.começar(); } while (thread.activeCount ()> 1) // Verifique se os threads anteriores foram concluídos thread.yield (); System.out.println (test.inc); }} Usando Lock:
Public class Test {public int Inc = 0; Trava de trava = new reentrantlock (); public void aument () {Lock.lock (); tente {inc ++; } finalmente {Lock.unlock (); }} public static void main (string [] args) {final teste de teste = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.começar(); } while (thread.activeCount ()> 1) // Verifique se os threads anteriores foram executados thread.yield (); System.out.println (test.inc); }} Usando atomicinteger:
Public class Test {public atomicinteger inc = new atomicInteger (); public void aument () {Inc.getAndIncrement (); } public static void main (string [] args) {final teste de teste = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.increase (); }; }.começar(); } while (thread.activeCount ()> 1) // Verifique se os threads anteriores foram executados thread.yield (); System.out.println (test.inc); }}Algumas classes de operação atômica são fornecidas sob o pacote Java.util.Concurrent.atomic de Java 1.5, a saber, o auto-incremento (adicione 1 operação), autodenagem (adicione 1 operação), operação de adição (adicione um número) e operação de subtração (adicione um número) dos tipos de dados básicos para garantir que essas operações de operações são operações atômicas. Atomic usa CAS para implementar operações atômicas (comparar e trocar). CAS é realmente implementado usando as instruções CMPXCHG fornecidas pelo processador, e o processador executa as instruções CMPXCHG é uma operação atômica.
3. pode garantir a ordem?
Como mencionado anteriormente, a palavra -chave volátil pode proibir a reordenação de instruções, portanto, é volátil garantir a ordem em certa medida.
Existem dois significados proibidos de reordenação de palavras -chave voláteis:
1) Quando o programa executa uma operação de leitura ou gravação da variável volátil, todas as alterações nas operações anteriores devem ter sido feitas e o resultado já está visível para as operações subsequentes; As operações subsequentes ainda não devem ter sido feitas;
2) Ao executar a otimização de instruções, a instrução acessada à variável volátil não pode ser colocada atrás dela, nem as declarações seguintes a variável volátil podem ser colocadas antes dela.
Talvez o que é dito acima seja um pouco confuso, então dê um exemplo simples:
// x e y são variáveis não voláteis // sinalizador é variável volátil x = 2; // declaração 1y = 0; // Declaração 2Flag = true; // declaração 3x = 4; // declaração 4y = -1; // Declaração 5
Como a variável de bandeira é uma variável volátil, ao executar o processo de reordenação de instruções, a Declaração 3 não será colocada antes da declaração 1 e 2, nem será colocada após a Declaração 3 e a Declaração 4 e 5. No entanto, não é garantido que a Ordem da Declaração 1 e a Declaração 2 e a Ordem da Declaração 4 e 5 não sejam garantidas.
Além disso, a palavra -chave volátil pode garantir que, quando a declaração 3 for executada, a Declaração 1 e a Declaração 2 devam ser executadas e os resultados da execução da Declaração 1 e da Declaração 2 estão visíveis para a Declaração 3, Declaração 4 e Declaração 5.
Então, vamos voltar ao exemplo anterior:
// Thread 1: context = loadContext (); // estado 1Initado = true; // Estado 2 // Thread 2: while (! Inited) {sleep ()} DosomethingWithConfig (contexto);Quando dei a este exemplo, mencionei que é possível que a declaração 2 seja executada antes da declaração 1, tanto tempo que ele pode fazer com que o contexto não seja inicializado e o Thread 2 usa o contexto não iniciado para operar, resultando em um erro do programa.
Se a variável initada for modificada com a palavra -chave volátil, esse problema não ocorrerá, porque quando a declaração 2 for executada, ela definitivamente garantirá que o contexto tenha sido inicializado.
4. O princípio e o mecanismo de implementação do volátil
A descrição anterior de alguns usos da palavra -chave volátil foi originária. Let’s discuss how volatile ensures visibility and prohibits instructions to reorder.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}参考资料:
《Java编程思想》
《深入理解Java虚拟机》
The above is all the content of this article. Espero que seja útil para o aprendizado de todos e espero que todos apoiem mais o wulin.com.