Prefácio
Às vezes, se você usa a sincronização apenas para ler e escrever um ou dois campos de instância, parece muito caro. A palavra-chave volátil fornece um mecanismo sem bloqueio para acesso síncrono aos campos da instância. Se um domínio for declarado como volátil, o compilador e a máquina virtual sabem que o domínio pode ser atualizado simultaneamente por outro thread. Antes de falar sobre a palavra -chave volátil, precisamos entender os conceitos relevantes do modelo de memória e as três características na programação simultânea: atomicidade, visibilidade e ordem.
1. Modelo de memória java com atomicidade, visibilidade e ordem
O modelo de memória Java estipula que todas as variáveis existem na memória principal e cada thread tem sua própria memória de trabalho. 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.
Em Java, execute a seguinte declaração:
int i = 3;
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 3 diretamente na memória principal.
Então, o que garante a própria linguagem Java fornece atomicidade, visibilidade e ordem?
Atomicidade
As operações de leitura e atribuição de variáveis do tipo 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.
Vamos dar uma olhada no seguinte código:
x = 10; // declaração 1y = x; // Declaração 2x ++; // declaração 3x = x + 1; // Declaração 4
Somente 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 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.
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.
Existem muitas classes no pacote Java.util.Concurrent.atômico que usam instruções muito eficientes no nível da máquina (em vez de bloqueios) para garantir a atomicidade de outras operações. Por exemplo, a classe AtomicInteger fornece métodos incrementos e decrementos e decrementos, que aumentam e diminuem respectivamente um número inteiro de maneira atômica. A classe AtomicInteger pode ser usada com segurança como um contador compartilhado sem sincronização.
Além disso, este pacote também contém classes atômicas, como atomicbooliano, atômico e atômica para programadores do sistema que desenvolvem apenas ferramentas simultâneas, e os programadores de aplicativos não devem usar essas classes.
Visibilidade
A visibilidade refere -se à visibilidade entre os threads e o estado modificado de um thread é visível para outro thread. Esse é o resultado de uma modificação do encadeamento. Outro tópico será visto imediatamente.
Quando uma variável compartilhada é modificada por volátil, garante que o valor modificado seja atualizado para a memória principal imediatamente, para que seja visível para outros threads. Quando outros threads precisam ler, 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.
Ordenado
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.
A palavra -chave volátil pode ser usada para garantir uma determinada "linha de pedidos". 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.
2. 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:
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; 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).
Por que é possível causar falha no thread na interrupção? Cada encadeamento tem sua própria memória de trabalho durante seu processo de execução. Quando o Thread 1 estiver em execução, ele copiará o valor da variável de parada e o coloca 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:
O volátil garante atomicidade?
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?
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(); } // Verifique se os threads anteriores concluíram a execução enquanto (Thread.ActiveCount ()> 1) Thread.yield (); System.out.println (test.inc); }} O resultado desse código é inconsistente toda vez que é executado. É um número menor que 10.000. Como mencionado anteriormente, a operação de incremento automático não é atômica. Inclui a leitura do valor original de uma variável, executando uma operação adicional 1 e escrevendo na memória de trabalho. Ou seja, as três sub-operações da operação de auto-incremento podem ser executadas separadamente.
Se o valor do Variable Inc for 10 em um determinado horário, o Thread 1 executará operação de auto-incremento na variável, o Thread 1 lê o valor original do Variable Inc e, em seguida, o thread 1 será 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 Variable 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 fará com que a linha de cache da variável de cache INC no thread 2 seja inválida na memória de trabalho, portanto, o Thread 2 irá diretamente para a memória principal para ler o valor da inc. Quando se vê que o valor do INC é 10, executa um aumento de 1 e escreve 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.
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 uma variável seja atômica.
O volátil 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:
3. Use a palavra -chave volátil corretamente
A palavra -chave sincronizada impede que vários threads executem uma parte do código ao mesmo tempo, o que afetará bastante a eficiência da execução do programa. O desempenho da palavra -chave volátil é melhor do que o sincronizado em alguns casos. No entanto, deve -se notar que a palavra -chave volátil não pode substituir a palavra -chave sincronizada, porque a palavra -chave volátil não pode garantir a atomicidade da operação. De um modo geral, as duas condições a seguir devem ser atendidas ao usar volátil:
A primeira condição é que não pode ser operações como auto-aumento e autodenagem. Como mencionado acima, o volátil não garante atomicidade.
Vamos dar um exemplo disso. Ele contém um invariante: o limite inferior é sempre menor ou igual ao limite superior.
classe pública NumberRange {private volátil int inferior, superior; public int getLower () {return mais baixo; } public int getUper () {return Upper; } public void setLower (int vale) {if (valor> superior) lança nova ilegalArgumentException (...); menor = valor; } public void setUper (int vale) {if (valor <inferior) lança nova ilegalArgumentException (...); superior = valor; }} Dessa forma, limita as variáveis de estado escopo, portanto, definir os campos inferiores e superiores como tipos voláteis não implementa completamente a segurança do encadeamento da classe e, portanto, ainda requer sincronização. Caso contrário, se dois threads executarem o setLower e o Setupper com valores inconsistentes ao mesmo tempo, o intervalo será inconsistente. Por exemplo, se o estado inicial for (0, 5), ao mesmo tempo, encaixe um setlower (4) e o Thread B chama o setapper (3), é óbvio que os valores armazenados no cruzado por essas duas operações não atendem às condições, os dois threads passam o cheque para proteger o invariante, de modo que o valor da última faixa é (4, 3), que é obtido.
De fato, é para garantir a atomicidade da operação para usar volátil. Existem dois cenários principais para o uso de voláteis:
Sinalizadores de status
Volátil Boolean ShutdownEsted; ... Public void Shutdown () {ShutdownRequested = true; } public void Dowork () {while (! ShutdownRequested) {// Do Stuff}}É provável que o método Shutdown () seja chamado de fora do loop - ou seja, em outro thread - para que algum tipo de sincronização precise ser executado para garantir que a visibilidade da variável de desligamento seja implementada corretamente. No entanto, escrever um loop com blocos sincronizados é muito mais problemático do que escrever com sinalizadores de status voláteis. Como o volátil simplifica a codificação e os sinalizadores de status não dependem de nenhum outro estado do programa, é muito adequado para o volátil aqui.
Modo de verificação dupla (DCL)
classe pública singleton {private volátil estático singleton instância = null; public static singleton getInstance () {if (instance == null) {synchronized (this) {if (instance == null) {instance = new singleton (); }} retornar a instância; }} O uso aqui volátil aqui afetará mais ou menos o desempenho, mas, considerando a correção do programa, ainda vale a pena sacrificar esse desempenho.
A vantagem do DCL é que ele possui alta utilização de recursos. Os objetos de Singleton são instanciados apenas quando o GetInstance é executado pela primeira vez, o que é altamente eficiente. A desvantagem é que a reação é um pouco mais lenta ao carregar pela primeira vez, e há certos defeitos em ambientes de alta concorrência, embora a probabilidade de ocorrência seja muito pequena.
Embora o DCL resolva os problemas de consumo de recursos, sincronização desnecessária, segurança do encadeamento etc. até certo ponto, ele ainda tem problemas de falha em alguns casos, ou seja, falha no DCL. No livro "Java Concurrency Programming Practice", recomenda o uso do seguinte código (padrão estático de classe interna singleton) para substituir o DCL:
classe pública singleton {private singleton () {} public static singleton getInstance () {return singletonsholder.sinstance; } classe estática privada singletonsholder {private estático final singleton sinstance = new singleton (); }} Sobre verificações duplas, você pode ver
4. Resumo
Comparado aos bloqueios, a variável volátil é muito simples, mas ao mesmo tempo um mecanismo de sincronização muito frágil que, em alguns casos, proporcionará melhor desempenho e escalabilidade do que os bloqueios. Se você seguir estritamente as condições de uso de voláteis que são realmente independentes de outras variáveis e de seus próprios valores anteriores, poderá usar volátil em vez de sincronizado para simplificar o código em alguns casos. No entanto, o código usando o volátil geralmente é mais propenso a erros do que o código usando bloqueios. Este artigo apresenta dois casos de uso mais comuns em que podem ser usados voláteis em vez de sincronizados. Em outros casos, é melhor usarmos sincronizados.
O exposto acima é tudo sobre este artigo, espero que seja útil para o aprendizado de todos.