O conteúdo principal deste artigo é um ponto de conhecimento comum na entrevista de Java: palavras -chave voláteis. Este artigo apresenta todos os aspectos das palavras -chave voláteis em detalhes. Espero que, depois de ler este artigo, você possa resolver perfeitamente os problemas relacionados de palavras -chave voláteis.
Em entrevistas de emprego relacionadas a Java, muitos entrevistadores gostam de examinar o entendimento do entrevistador sobre a simultaneidade de Java. Usando a palavra -chave volátil como um pequeno ponto de entrada, muitas vezes você pode perguntar ao modelo de memória Java (JMM) e alguns recursos da programação simultânea Java. Em profundidade, você também pode examinar a implementação da JVM subjacente e o conhecimento relacionado ao sistema operacional. Vamos fazer um processo de entrevista hipotético para obter uma compreensão profunda da palavra-chave volítima!
Até onde eu entendi, as variáveis compartilhadas modificadas por voláteis têm as duas características a seguir:
1. Verifique a visibilidade da memória de diferentes encadeamentos para a operação variável;
2. Proibir o comando reordenando
Isso é muito o que falar, então começarei com o modelo de memória Java. O Java Virtual Machine Specification tenta definir um modelo de memória Java (JMM) para bloquear as diferenças de acesso à memória entre vários sistemas de hardware e operação, para que os programas Java possam obter efeitos consistentes de acesso à memória em várias plataformas. Simplificando, como a CPU executa instruções muito rapidamente, a velocidade de acesso à memória é muito mais lenta e a diferença não é uma ordem de magnitude, os grandes caras que trabalham no processador adicionaram várias camadas de cache à CPU. No modelo de memória Java, a otimização acima é abstraída novamente. O JMM estipula que todas as variáveis estão na memória principal, semelhante à memória comum mencionada acima, e cada encadeamento contém sua própria memória de trabalho. Pode ser considerado como um registro ou cache na CPU para obter fácil entendimento. Portanto, as operações de threads são baseadas principalmente na memória de trabalho. Eles só podem acessar sua própria memória de trabalho e devem sincronizar o valor de volta à memória principal antes e depois do trabalho. Eu nem sei o que eu disse, pegue um pedaço de papel para desenhar:
Ao executar um encadeamento, o valor da variável será lido pela primeira vez na memória principal, depois carregado na cópia na memória de trabalho e depois a passou para o processador para execução. Após a conclusão da execução, a cópia na memória de trabalho receberá um valor e, em seguida, o valor na memória de trabalho será transmitido de volta à memória principal e o valor na memória principal será atualizado. Embora o uso da memória de trabalho e da memória principal seja mais rápido, ela também traz alguns problemas. Por exemplo, veja o seguinte exemplo:
i = i + 1;
Supondo que o valor inicial de I seja 0, quando apenas um thread o executar, o resultado definitivamente receberá 1. Quando dois threads forem executar, o resultado receberá 2? Este não é necessariamente o caso. Este pode ser o caso:
Tópico 1: Carregar i da memória principal // i = 0 i + 1 // i = 1 Tópico 2: Carregar I da memória principal // porque o thread 1 não escreveu o valor de I de volta à memória principal, eu ainda é 0 i + 1 // i = 1 Tópico 1: Salvar i para a memória principal Tópico 2: Salvar i na memória principal
Se dois threads seguirem o processo de execução acima, o último valor de I é realmente 1. Se a última gravação é lenta e você poderá ler o valor de I novamente, pode ser 0, que é o problema de inconsistência do cache. O seguinte é mencionar a pergunta que você acabou de fazer. O JMM é estabelecido principalmente sobre como lidar com as três características de atomicidade, visibilidade e ordem no processo de simultaneidade. Ao resolver esses três problemas, o problema da inconsistência do cache pode ser resolvido. E volátil está relacionado à visibilidade e ordem.
1. Atomicidade: em Java, as operações de leitura e atribuição dos tipos de dados básicos são operações atômicas. As chamadas operações atômicas significam que essas operações são ininterruptas e devem ser concluídas por um certo período de tempo, ou não serão executadas. por exemplo:
i = 2; j = i; i ++; i = i+1;
Entre as quatro operações acima, i = 2 é uma operação de leitura, que deve ser uma operação atômica. j = Eu acho que é uma operação atômica. De fato, é dividido em duas etapas. Um é ler o valor de i e atribuir o valor a j. Esta é uma operação em duas etapas. Não pode ser chamado de operação atômica. i ++ e i = i+ 1 são realmente equivalentes. Leia o valor de i, adicione 1 e escreva -o de volta à memória principal. Essa é uma operação em três etapas. Portanto, no exemplo acima, o último valor pode ter muitas situações porque não pode satisfazer a atomicidade. Dessa maneira, há apenas uma leitura simples. A atribuição é uma operação atômica ou apenas atribuição numérica. Se você usa variáveis, existe uma operação adicional para ler o valor da variável. Uma exceção é que a especificação da máquina virtual permite que os tipos de dados de 64 bits (longos e duplos) sejam processados em 2 operações, mas a mais recente implementação do JDK ainda implementa operações atômicas. O JMM implementa apenas a atomicidade básica. Operações como o I ++ acima devem ser sincronizadas e travar para garantir a atomicidade de todo o código. Antes que o thread libere o bloqueio, ele inevitavelmente escovará o valor de I de volta à memória principal. 2. Visibilidade: Falando em visibilidade, o Java usa o VOLATILE para fornecer visibilidade. Quando uma variável é modificada por volátil, a modificação será atualizada imediatamente na memória principal. Quando outros threads precisam ler a variável, o novo valor será lido na memória. Isso não é garantido por variáveis comuns. De fato, sincronizado e bloqueio também podem garantir a visibilidade. Antes que o encadeamento libere a trava, ele liberará todos os valores de variáveis compartilhados de volta à memória principal, mas sincronizados e bloqueiam são mais caros. 3. O pedido do JMM permite que o compilador e o processador reordenem as instruções, mas estipula a semântica como serial, ou seja, não importa quão reordenar, o resultado da execução do programa não pode ser alterado. Por exemplo, o seguinte segmento de programa:
Pi duplo = 3,14; // adado r = 1; // bdouble s = pi * r * r; // c
A instrução acima pode ser executada em A-> B-> C, com o resultado sendo 3,14, mas também pode ser executado na ordem de B-> a-> C. Como A e B são duas declarações independentes, enquanto C depende de A e B, A e B podem ser reordenadas, mas C não pode ser classificado em primeiro lugar em A e B. JMM garante que a reordenação não afete a execução de um único fio, mas os problemas tendem a ocorrer em múltiplas autores. Por exemplo, código como este:
int a = 0; bandeira bool = false; public void write () {a = 2; // 1 sinalizador = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Se dois threads executarem o segmento de código acima, o thread 1 executa primeiro gravar, depois o thread 2 e executará multiplicar, o valor do Ret deverá ser 4? O resultado não é necessariamente:
Como mostrado na figura, 1 e 2 no método de gravação são reordenados. O thread 1 primeiro atribui o sinalizador ao TRUE e depois o executa no thread 2, o RET calcula diretamente o resultado e, em seguida, o thread 1. Nesse momento, a é atribuído a 2, o que é obviamente um passo depois. No momento, você pode adicionar a palavra -chave volátil ao sinalizador, proibir a reordenação, o que pode garantir a ordem do programa, e você também pode usar sincronizado e travar os pesos pesados para garantir a ordem. Eles podem garantir que o código nessa área seja executado ao mesmo tempo. Além disso, o JMM tem alguma ordem inata, ou seja, a ordem que pode ser garantida sem nenhum meio, que geralmente é chamada de princípio. << JSR-133: Modelo de memória Java e especificação de threads >> Define o seguinte acontece-antes de regras: 1. Regras de sequência do programa: Para cada operação em um fio, acontece antes de ser usado para qualquer operações subsequentes no domínio do segmento 2. Domínio volátil 4. Transitividade: se A acontecer antes de B e B acontecer, antes de C, Aconte-se antes de C 5.Start () Regras: Se o thread a executará uma operação Threadb_start () (shall thread b), então sndingb_start () ocorrer antes da operação arbitrária de Thread A. Acontece que o thread A retorna com sucesso da operação Threadb.join () no Thread A. 7. Interrupt () Princípio: a chamada para o método de interrupção de thread () ocorre primeiro quando o evento de interrupção é detectado pelo código do encadeamento interrompido. Você pode usar o método Thread.Interrupted () para detectar se há uma interrupção. 8. Princípio Finalize (): A conclusão de inicialização de um objeto ocorre primeiro quando o método Finalize () começa. A primeira regra da regra da sequência do programa diz que, em um thread, todas as operações estão em sequência, mas no JMM, desde que o resultado da execução seja o mesmo, a reordenação é permitida. O foco de acontece antes aqui também é a correção do resultado da execução de thread único, mas não pode ser garantido que o mesmo se aplica ao multi-threading. Regra 2 As regras do monitor são realmente fáceis de entender. Antes de adicionar o bloqueio, você só pode continuar a adicionar o bloqueio. A regra 3 se aplica ao volátil em questão. Se um thread gravar uma variável primeiro e outro thread o ler, a operação de gravação deve estar antes da operação de leitura. A quarta regra é a transitividade de acontecer antes. Não vou entrar em detalhes sobre os seguintes.
Em seguida, precisamos re-mencionar regras variáveis voláteis: escreva um domínio volátil, acontece antes de ler este domínio volátil posteriormente. Deixe -me tirar isso de novo. De fato, se uma variável for declarada como volátil, quando leio a variável, sempre posso ler seu valor mais recente. Aqui, o valor mais recente significa que, independentemente de qual outro thread grava a variável, ele será atualizado imediatamente para a memória principal. Também posso ler o valor recém -escrito da memória principal. Em outras palavras, a palavra -chave volátil pode garantir visibilidade e ordem. Vamos tomar o código acima como exemplo:
int a = 0; bandeira bool = false; public void write () {a = 2; // 1 sinalizador = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Esse código não está apenas perturbado pela reordenação, mesmo que 1 e 2 não sejam reordenados. 3 também não será executado tão suavemente. Suponha que o thread 1 execute a operação de gravação primeiro e o thread 2 e depois execute a operação multiplique. Como o thread 1 atribui o sinalizador a 1 na memória de trabalho, ele não pode ser gravado de volta à memória principal imediatamente. Portanto, quando o thread 2 é executado, multiplique o valor do sinalizador da memória principal, que ainda pode ser falso, para que as instruções entre colchetes não sejam executadas. Se alterado para o seguinte:
int a = 0; bandeira volátil bool = false; public void write () {a = 2; // 1 sinalizador = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Em seguida, o thread 1 executa escreva primeiro e o thread 2 e executa multiplicar. De acordo com o princípio acontecer antes, esse processo satisfazer os três tipos de regras a seguir: Regras da ordem do programa: 1 acontece antes de 2; 3 acontece antes de 4; (A volátil restringe a reordenação de instruções, então 1 é executado antes de 2) Regras voláteis: 2 acontece antes de 3 regras transitivas: 1 acontece antes de 4 Ao escrever uma variável volátil, o JMM liberará a variável compartilhada na memória local correspondente ao encadeamento à memória principal. Ao ler uma variável volátil, o JMM definirá a memória local correspondente ao encadeamento para invalidar e o thread lerá a variável compartilhada da memória principal a seguir.
Primeiro de tudo, minha resposta é que a atomicidade não pode ser garantida. Se for garantido, é apenas atomicidade para a leitura/escrita de uma única variável volátil, mas não há nada a ver com operações compostas como volátil ++, como o exemplo a seguir:
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); }Logicamente falando, o resultado é de 10.000, mas é provável que seja um valor inferior a 10.000 ao ser executado. Algumas pessoas podem dizer que a volátil não garante visibilidade. Um tópico deve ver as modificações no INC da Inc, e o outro tópico deve vê -lo imediatamente! Mas a Operação Inc ++ aqui está uma operação composta, incluindo a leitura do valor do INC, aumentando -o por si só e, em seguida, escrevendo -o de volta à memória principal. Suponha que o Thread A leia o valor do INC para ser 10 e esteja bloqueado no momento porque a variável não é modificada e a regra volátil não pode ser acionada. O thread B também lê o valor do INC no momento. O valor do inc na memória principal ainda é 10 e será aumentado automaticamente e, em seguida, será escrito de volta à memória principal, que é 11. Nesse momento, é a vez do Thread A para executar. Como 10 é salvo na memória de trabalho, ele continua a aumentar a si mesmo e grava de volta à memória principal. 11 está escrito novamente. Portanto, embora os dois threads executassem o aumento () duas vezes, eles adicionaram apenas uma vez. Algumas pessoas dizem que não invalida a linha de cache? No entanto, antes que o Thread A Reads Thread B e execute operações, o valor do INC não é modificado; portanto, quando o Thread B lê, ele ainda lê 10. Algumas pessoas também dizem que, se o thread B gravar 11 de volta à memória principal, não definirá a linha de cache do Thread A para invalidar? No entanto, o thread A já fez a operação de leitura. Somente quando a operação de leitura é feita e a linha de cache é inválida, ela lerá o valor principal da memória. Portanto, o thread a pode continuar a fazer auto-incrementidade. Para resumir, nesse tipo de operação composta, a função atômica não pode ser mantida. No entanto, no exemplo acima da definição do valor do sinalizador, como a operação de leitura/gravação dos sinalizadores é uma etapa única, ele ainda pode garantir a atomicidade. Para garantir a atomicidade, só podemos usar classes de operação sincronizadas, de trava e atômico em pacotes simultâneos, ou seja, o auto-incremento (adicione 1 operação), autodenagem (redução de 1 operação), operação de adição (adicione várias operações) e operação de subtração (subtrair um número) dos tipos de dados básicos para garantir que essas operações sejam operações atômicas.
Se você gerar código de montagem com a palavra -chave volátil e o código sem a palavra -chave volátil, descobrirá que o código com a palavra -chave volátil terá uma instrução adicional de prefixo de bloqueio. A instrução de prefixo de bloqueio é realmente equivalente a uma barreira de memória. A barreira da memória fornece as seguintes funções: 1. Ao reordenar, as seguintes instruções não podem ser reordenadas ao local antes da barreira da memória 2. Faça o cache desta CPU escrita na memória ** ** 3. A ação de gravação também causará outras cpus ou outros manobros a outros threads.
1. Marca de quantidade de status, assim como a bandeira acima, vou mencioná -la novamente:
int a = 0; bandeira volátil bool = false; public void write () {a = 2; // 1 sinalizador = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Essa operação de leitura e gravação para variáveis, marcadas como voláteis, pode garantir que as modificações sejam imediatamente visíveis para o thread. Comparado com o sincronizado, o bloqueio tem uma certa melhoria de eficiência. 2. Implementação do modo singleton, bloqueio típico de verificação dupla (DCL)
classe singleton {private volátil estático singleton instance = null; private singleton () {} public static singleton getInstance () {if (instance == null) {synchronized (singleton.class) {if (instance == null) instância = new singleton (); }} retornar a instância; }}Esse é um padrão preguiçoso de singleton, os objetos são criados apenas quando usados e, para evitar reordenar instruções para operações de inicialização, é adicionado à instância volátil.
O exposto acima é o conteúdo inteiro deste artigo sobre a explicação das palavras -chave voláteis que os entrevistadores da Java adoram perguntar em detalhes. Espero que seja útil para todos. Amigos interessados podem continuar se referindo a outros tópicos relacionados neste site. Se houver alguma falha, deixe uma mensagem para apontá -la. Obrigado amigos pelo seu apoio para este site!