Sabemos que as operações simultâneas implementadas pelo Java devem ser finalmente concluídas pela nossa CPU. Enquanto isso, compilamos o código -fonte Java em um arquivo .class, depois carregado e executado pelo mecanismo de execução da máquina virtual, interpretado como linguagem de montagem, depois convertido em instruções operacionais do sistema e depois convertido em 1, 0 e, finalmente, a CPU é reconhecida e executada.
Quando mencionamos a concorrência do Java, não podemos deixar de pensar em palavras -chave comuns em Java: volátil e sincronizado. Em seguida, analisamos -os a partir dessas duas palavras de desligamento:
O princípio de implementação subjacente do volátil
Princípios de implementação e aplicações de sincronizado
volátil
Falando em volátil, o entrevistador é a pergunta favorita a fazer em entrevistas em Java. Quando o vemos, a primeira coisa em que pensamos é manter a visibilidade entre os threads. É um sincronizado leve, que em alguns casos pode substituir sincronizado.
O papel da volátil:
Para uma variável modificada por volátil, o modelo de memória Java garantirá que os valores variáveis vistos por todos os threads sejam consistentes.
Como funciona o volátil:
Podemos definir uma variável volátil, atribuir valores de TI e usar ferramentas para obter as instruções de montagem geradas pelo compilador JIT. Descobriremos que, ao escrever para a variável volátil, haverá uma instrução adicional: uma instrução prefixada com bloqueio:
A instrução de prefixo de trava causa duas coisas de volta ao processador multi-núcleo:
① Escreva de volta os dados da linha de cache do processador atual na memória.
②Estas a operação de memória de gravação de gravação invalidará o endereço de memória em cache de dados em outras CPUs.
Quando conhecemos os dois pontos acima, não é difícil entender o mecanismo da variável Volatie.
Em vários processadores, para garantir que o cache de cada processador seja consistente, um protocolo de consistência do cache será implementado. Cada processador fareja os dados propagados no barramento para verificar se o valor em cache expirou.
sincronizado
Ao pensar na simultaneidade de vários thread, a primeira coisa que penso é sincronizada. Traduzido como sincronização. Todos sabemos que é uma fechadura pesada. Ao usá -lo para um bloqueio de método ou código, quando um thread obtém esse bloqueio, outros threads caem em um estado suspenso, que aparecerá no estado do sono em Java. Todos sabemos que a suspensão e o tempo de execução do thread devem ser transferidos para o estado do kernel do sistema operacional (o correspondente ao estado do kernel é o estado do usuário), o que é particularmente desperdiçado de recursos da CPU; portanto, esse bloqueio pesado é verdadeiro!
No entanto, após o Java SE 1.6, a equipe de manutenção de Java realizou uma série de otimizações (essas otimizações são discutidas uma a uma), por isso não é tão "pesado" e a trava reentrante, que tinha vantagens no passado, se tornou menos vantajosa (reentrantlock).
Vamos falar sobre sincronizado nos seguintes aspectos:
O básico do sincronizado para obter sincronização
Como os implementos sincronizados travam
Trava positiva, trava leve (trava de rotação), trava pesada
Atualização de bloqueio
Como implementar operações atômicas em Java
①O básico do sincronizado para obter sincronização:
Podemos ver sincronizadas no desenvolvimento ou no código -fonte Java, como hashtable, stringbuilder e outros lugares. Existem duas maneiras comuns:
Ⅰ, método de sincronização
O método de sincronização só precisa ser sincronizado antes do método. Quando um thread o executar, outros threads caem em espera até que solte a trava. O uso de métodos pode ser dividido em dois tipos: para métodos de sincronização comuns e para métodos estáticos. A diferença entre eles é que os objetos bloqueados são diferentes. A posição bloqueada dos métodos comuns é o objeto atual e a posição bloqueada dos métodos estáticos é o objeto de classe da classe atual.
Ⅱ, bloco de método de sincronização
O bloqueio do método de sincronização bloqueia o objeto configurado nos colchetes após sincronizado. Este objeto pode ser um valor e qualquer variável ou objeto.
②Como implementos sincronizados Lock:
Na especificação da JVM, você pode ver o princípio de implementação de sincronizado na JVM. A JVM implementa a sincronização de métodos de sincronização e blocos de código com base na entrada e saída do objeto Monitor. O bloco de código é implementado usando as instruções MonitorEnter e Monitorexit. O método de sincronização não é dado especificamente na especificação da JVM. No entanto, acredito que os princípios específicos devem ser diferentes. Não é nada mais do que compilar o código -fonte Java em um arquivo de classe e marcar o método sincronizado no arquivo ByteCode de classe. Este método será sincronizado quando o mecanismo bytecode executar esse método.
③defleção trava, trava leve (trava de spin), trava pesada:
Antes de falar sobre bloqueios, precisamos conhecer o cabeçalho do objeto Java e o cabeçalho do objeto Java:
A trava usada pelo sincronizada é armazenada no cabeçalho do objeto Java. O cabeçalho do objeto Java possui 32 bits/64 bits (dependendo do número de bits do sistema operacional) o comprimento do MarkWord armazena o código de hash e bloqueia as informações do objeto. Existem espaços de 2 bits no Markword para representar o estado da trava 00, 01, 10, 11, respectivamente, representando bloqueios leves, travas de polarização, bloqueios de peso pesado e marcas GC.
Bloqueio positivo: o bloqueio positivo é chamado de bloqueio excêntrico. Pelo nome, podemos ver que é uma fechadura que tende a um determinado fio.
No desenvolvimento real, descobrimos que a simultaneidade de vários thread, a maioria dos métodos de sincronização é realizada pelo mesmo encadeamento, e a probabilidade de vários threads competindo por um método é relativamente baixo, portanto, a aquisição repetida e a liberação de bloqueios causarão muito resíduos de recursos. Portanto, para fazer com que o encadeamento obtenha um bloqueio a um custo menor, a trava do viés é introduzida. Quando um thread acessa um bloco de sincronização e adquire um bloqueio, o ID do encadeamento da trava do viés será armazenado no registro de bloqueio no quadro da pilha do cabeçalho do objeto e do encadeamento. No futuro, quando o encadeamento entra e sai do bloco de sincronização, ele não precisa executar operações de CAS para travar e desbloquear. Só é necessário simplesmente verificar se existe uma trava de viés apontando para a palavra de marca atual no cabeçalho do objeto (no Markword, há um bit de bandeira de trava de viés para indicar se o objeto atual suporta bloqueio de viés. Podemos usar o parâmetro JVM para definir o bloqueio de viés).
Em relação à liberação de bloqueios tendenciosos, os bloqueios tendenciosos usam o mecanismo de liberação da trava até que exista a concorrência, de modo que a rosca que mantém a trava tendenciosa liberará a trava quando outros threads tentarem competir por bloqueios tendenciosos.
NOTA: Em Java6, 7, o bloqueio de preconceito é iniciado por padrão
Bloqueio leve:
Um bloqueio leve é que, antes de executar o bloco de sincronização, a JVM criará um espaço para armazenar o registro de bloqueio no quadro da pilha do encadeamento atual e copiará o MarkWord no cabeçalho do objeto nele. Em seguida, o thread tentará substituir o Markword no cabeçalho do objeto por um ponteiro para o registro de bloqueio. Se for bem -sucedido, o encadeamento atual obterá o bloqueio. Se falhar, significa que outros threads competem pelo bloqueio e o encadeamento atual girará para obter o bloqueio.
④ Atualização de bloqueio:
Se o thread atual não puder tentar o método acima para obter o bloqueio, significa que o bloqueio atual está em competição e o bloqueio será atualizado para uma trava pesada.
A diferença entre trava leve e trava tendenciosa:
As bloqueios leves usam operações de CAS para eliminar mutexes usados em sincronização sem concorrência, enquanto bloqueios tendenciosos removem toda a sincronização sem concorrência sem concorrência, e até as operações do CAS não são feitas!
⑤ Como implementar operações atômicas em Java:
Antes de entender como o Java implementa operações atômicas, precisamos saber como os processadores implementam operações atômicas:
Os processadores geralmente são divididos em duas maneiras de executar operações atômicas: bloqueio de cache e bloqueio de barramento, entre os quais o bloqueio de cache é melhor, enquanto o bloqueio de barramento é mais consumidor de recursos. (Não explicaremos muito sobre os dois métodos de travamento aqui, mas haverá explicações detalhadas no sistema operacional)
O Java usa (principalmente) CAS para implementar operações atômicas, mas o uso do CAS para implementar operações atômicas também causará alguns dos seguintes problemas clássicos:
1) Problema ABA
A classe AtomicstampedReference é fornecida no JDK para resolver (fornecendo referências esperadas para verificação e bandeiras esperadas)
2) Tempo de ciclo longo e alta sobrecarga
Não posso resolver isso, este é um problema comum de circulação
3) Somente operações atômicas de uma variável compartilhada podem ser garantidas
Uma atômica é fornecida no JDK para resolver o problema, colocando várias variáveis compartilhadas em uma classe para operações do CAS.