Primeiro, vamos introduzir alguns bloqueios otimistas e bloqueios pessimistas:
Bloqueio pessimista: sempre assuma o pior caso. Toda vez que vou obter os dados, acho que os outros o modificarão, então o travarei toda vez que recebo os dados, para que os outros o bloquearão até que eles recebam o bloqueio. Os bancos de dados relacionais tradicionais usam muitos desses mecanismos de travamento, como travas de linha, travas de tabela, etc., travas de leitura, bloqueios de gravação etc., que estão bloqueados antes de fazer operações. Por exemplo, a implementação da palavra -chave sincronizada sincronizada no Java também é um bloqueio pessimista.
Bloqueio otimista: como o nome sugere, significa muito otimista. Toda vez que vou buscar os dados, acho que outros não o modificam, então não o travarei. No entanto, ao atualizar, julgarei se outros atualizaram os dados durante esse período e podem usar o número da versão e outros mecanismos. Os bloqueios otimistas são adequados para tipos de aplicativos de leitura múltipla, que podem melhorar a taxa de transferência. Por exemplo, o banco de dados fornece uma trava otimista semelhante ao mecanismo Write_condition, mas eles são realmente fornecidos por bloqueios otimistas. Em Java, a classe variável atômica sob o pacote java.util.Concurrent.atomic é implementada pelo CAS usando uma trava otimista.
Uma implementação de bloqueio otimista (Compare e Swap):
Problemas de travamento:
Antes de JDK1.5, o Java confiava em palavras -chave sincronizadas para garantir a sincronização. Dessa forma, usando um protocolo de bloqueio consistente para coordenar o acesso ao estado compartilhado, ele pode garantir que, independentemente de qual encadeamento mantenha o bloqueio de variáveis compartilhadas, ele usa um método exclusivo para acessar essas variáveis. Este é um tipo de bloqueio exclusivo. A trava exclusiva é na verdade uma espécie de trava pessimista, por isso pode -se dizer que sincronizado é uma trava pessimista.
O mecanismo de bloqueio pessimista tem os seguintes problemas:
1. Sob competição com vários thread, a adição e liberação de bloqueios levará a mais atrasos de comutação e agendamento de contexto, causando problemas de desempenho.
2. Um fio que segura uma trava causará todos os outros threads que exigem que essa trava pendure.
3. Se um thread com alta prioridade aguarda que um thread com baixa prioridade libere o bloqueio, causará inversão prioritária, causando riscos de desempenho.
Comparado com esses problemas de bloqueios pessimistas, outro bloqueio mais eficaz são os bloqueios otimistas. De fato, o bloqueio otimista é: toda vez que você não adiciona um bloqueio, mas completa uma operação, assumindo que não há conflito simultâneo. Se o conflito simultâneo falhar, tente novamente até que seja bem -sucedido.
Bloqueio otimista:
O bloqueio otimista foi mencionado acima, mas na verdade é um tipo de pensamento. Comparados com bloqueios pessimistas, os bloqueios otimistas assumem que os dados geralmente não causam conflitos simultâneos; portanto, quando os dados são enviados e atualizados, eles detectarão formalmente se os dados têm conflitos simultâneos. Se um conflito simultâneo for encontrado, as informações erradas do usuário serão retornadas e o usuário decide como fazê -lo.
O conceito de bloqueio otimista mencionado acima explicou seus detalhes específicos da implementação: inclui principalmente duas etapas: detecção de conflitos e atualização de dados. Um dos métodos típicos de implementação é comparado e troca (CAS).
CAS:
CAS é uma tecnologia de travamento otimista. Quando vários threads tentam usar o CAS para atualizar a mesma variável ao mesmo tempo, apenas um dos threads pode atualizar o valor da variável, enquanto os outros threads falham. O encadeamento fracassado não será suspenso, mas será informado de que essa competição falhou e pode tentar novamente.
A operação CAS contém três operandos - o local da memória (v) que precisa ser lido e escrito, o valor original esperado (a) para comparação e o novo valor (b) a ser gravado. Se o valor da posição da memória v corresponder ao valor original esperado a, o processador atualizará automaticamente o valor da posição para o novo valor B. Caso contrário, o processador não fará nada. Em ambos os casos, ele retorna o valor desse local antes da diretiva CAS. (Em alguns casos especiais de CAS, apenas se o CAS é bem -sucedido ou não, sem extrair o valor atual.) CAS afirma efetivamente que "acho que a posição v deve conter o valor a; se ele contiver, coloque B nessa posição; caso contrário, não altere a posição, basta me dizer o valor atual dessa posição". Na verdade, é o mesmo que o princípio da verificação de conflitos + atualização de dados de bloqueios otimistas.
Deixe -me enfatizar aqui que o bloqueio otimista é um tipo de pensamento. CAS é uma maneira de perceber essa ideia.
Apoio ao Java para CAS:
O novo java.util.Concurrent (JUC) no JDK1.5 é construído no CAS. Comparado com algoritmos de bloqueio como sincronizados, o CAS é uma implementação comum de algoritmos não bloqueadores. Portanto, a JUC melhorou bastante seu desempenho.
Pegue atomicinteger em java.util.Concurrent como exemplo para ver como garantir a segurança do thread sem usar bloqueios. Entendemos principalmente o método GetAndIncrement, que é equivalente à operação ++ I.
A classe pública AtomicInteger estende o número implementa Java.io.Serializable {private Volátil Int Value; public final int get () {Return Value; } public final int getAndIncrement () {for (;;) {int current = get (); int próximo = corrente + 1; if (comparaandndSet (atual, próximo)) retornar corrente; }} public final boolean ComparaNDSet (int espera, int update) {return unsafe.compareandswapint (este, valueoffset, espera, atualização); }}No mecanismo sem bloqueios, o valor do campo deve ser usado para garantir que os dados entre os threads sejam visibilidade. Dessa forma, você pode ler diretamente quando obtém o valor de uma variável. Então vamos ver como o ++ eu é feito.
O GetandIncrement usa operação do CAS e, cada vez que você lê dados da memória, executa a operação CAS nesses dados e o resultado após +1. Se for bem -sucedido, o resultado será devolvido; caso contrário, tente novamente até ficar bem -sucedido.
ComparaDSet usa o JNI (Java Native Interface) para concluir a operação das instruções da CPU:
public final boolean ComparaDSet (int espera, int update) {return unsefa.compareandswapint (este, valueoffset, espera, atualização); }onde insefe.compareandswapint (isto, valueoffset, espera, atualização); é semelhante à seguinte lógica:
if (this == espera) {this = update retorna true; } else {return false; }Então, como comparar isso == Espere, substitua esta = atualização, comparandswapint para alcançar a atomicidade dessas duas etapas? Consulte os princípios do CAS
Princípio do CAS:
O CAS é implementado chamando o código JNI. O comparandswapint é implementado usando C para chamar as instruções subjacentes da CPU.
A seguir, explica o princípio da implementação do CAS a partir da análise da CPU mais usada (Intel X86).
Aqui está o código -fonte do método comparaandswapInt () da classe Sun.misc.unsfe:
Public Final Native Boolean Comparanswapint (objeto O, deslocamento longo, esperado, int x);
Você pode ver que esta é uma chamada de método local. O código C ++ que esse método local chama no JDK é:
#Define Lock_IF_MP (MP) __ASM CMMP MP, 0 / __ASM JE L0 / __ASM _EMIT 0XF0 / __ASM L0: Jint Atomic :: cmpxchg (Jint Exchange_Value, Volatile Jint* destin, JiNeCear_value) {/ alternativo para o alterno, para o alternative; __asm {mov edx, dest mov ecx, Exchange_value mov eax, compare_value lock_if_mp (mp) cmmpxchg dword ptr [edx], ecx}}Conforme mostrado no código -fonte acima, o programa decidirá se deve adicionar um prefixo de bloqueio à instrução CMMPXCHG com base no tipo do processador atual. Se o programa estiver em execução em um multiprocessador, adicione o prefixo de bloqueio à instrução CMMPXCHG. Pelo contrário, se o programa estiver em execução em um único processador, o prefixo de bloqueio será omitido (o próprio processador único mantém a consistência seqüencial no processador único e não requer o efeito da barreira de memória fornecido pelo prefixo de bloqueio).
Desvantagens do CAS:
1. Perguntas da ABA:
Por exemplo, se um thread, um retirar a da Posição da Memória V, outro thread dois também retira uma de memória e dois executa algumas operações e se tornará B, e depois dois giram os dados na posição V A. Nesse momento, o Thread One executa operação CAS e descobre que A ainda está na memória e depois opera com sucesso. Embora a operação do CAS do Thread One seja bem -sucedida, pode haver problemas ocultos. Como mostrado abaixo:
Há uma pilha implementada com uma lista vinculada de mão única, com a parte superior da pilha sendo A. Nesse momento, o thread T1 já sabe que o A.Next é B e, em seguida, espera substituir a parte superior da pilha por B por Cas:
head.compareandsset (a, b);
Antes de T1 executar a instrução acima, o Thread T2 intervém, coloca A e B da pilha e, em seguida, Pushd, C e A. Nesse momento, a estrutura da pilha é a seguinte, e o objeto B está em um estado livre neste momento:
Neste momento, é a vez do Thread T1 para executar a operação CAS. A detecção constatou que o topo da pilha ainda é A, então o CAS é bem -sucedido, e o topo da pilha se torna B, mas na verdade o B.Next é nulo, então a situação neste momento se torna:
Existe apenas um elemento B na pilha, e a lista vinculada composta de C e D não existe mais na pilha. C e D são jogados fora sem motivo.
A partir do Java 1.5, o pacote atômico do JDK fornece uma classe atomicstampedReference para resolver o problema do ABA. O método ComparaDSet desta classe é primeiro verificar se a referência atual é igual à referência esperada e se o sinalizador atual é igual ao sinalizador esperado. Se todos forem iguais, a referência e o valor do sinalizador são definidos para o valor atualizado fornecido de maneira atômica.
Public Boolean Comparansset (v Espera -Referência, // Referência esperada contra NewReference, // Referência Atualizada no Esperou
Código de aplicativo real:
private estático atomicstampedReference <Integer> atomicstampedRef = new AtomicstampedReference <Integer> (100, 0); ......... atomicstampedref.comparenderndset (100, 101, carimbo, carimbo + 1);
2. Tempo de ciclo de longa e alta sobrecarga:
O Spin Cas (se falhar, será executado com ciclismo até que seja bem -sucedido) se falhar por um longo tempo, trará uma ótima sobrecarga de execução para a CPU. Se a JVM puder suportar as instruções de pausa fornecidas pelo processador, a eficiência será aprimorada até certo ponto. As instruções de pausa têm duas funções. Primeiro, ele pode atrasar as instruções de execução do pipeline (despipline) para que a CPU não consuma muitos recursos de execução. O tempo de atraso depende da versão de implementação específica. Em alguns processadores, o tempo de atraso é zero. Segundo, ele pode evitar a descarga do oleoduto da CPU causada pela violação da ordem da memória ao sair do loop, melhorando assim a eficiência da execução da CPU.
3. Somente operações atômicas de uma variável compartilhada podem ser garantidas:
Ao executar operações em uma variável compartilhada, podemos usar o método CAS cíclico para garantir operações atômicas. No entanto, ao operar várias variáveis compartilhadas, o CAS cíclico não pode garantir a atomicidade da operação. Neste momento, você pode usar bloqueios ou há um truque, que é mesclar várias variáveis compartilhadas em uma variável compartilhada para operar. Por exemplo, existem duas variáveis compartilhadas i = 2, j = a, mescla ij = 2a e, em seguida, use CAS para operar IJ. A partir do Java 1.5, o JDK fornece a classe AtomicReference para garantir a atomicidade entre os objetos referenciados. Você pode colocar várias variáveis em um objeto para operação CAS.
CEs e cenários de uso sincronizado:
1. Para situações em que há menos concorrência de recursos (conflito de threads leves), usando o bloqueio de sincronização sincronizado para bloqueio de roscas e operações de comutação e comutação de despertar entre os estados do kernel do estado de usuário é um desperdício extra de recursos da CPU; Embora o CAS seja implementado com base no hardware, não precisa entrar no kernel, não precisa alterar os threads e a chance de operar rotações é menor, portanto, um desempenho mais alto pode ser obtido.
2. Para situações em que a concorrência de recursos é grave (conflito grave de threads), a probabilidade de rotação do CAS é relativamente alta, o que desperdiça mais recursos da CPU e é menos eficiente que o sincronizado.
Suplemento: O sincronizado foi melhorado e otimizado após o JDK1.6. A implementação subjacente de sincronizada baseia-se principalmente na fila do bloqueio livre. A idéia básica é bloquear após o spin, continuar competindo por bloqueios após a comutação da competição, sacrificando levemente a justiça, mas obtendo alta taxa de transferência. Quando há menos conflitos de threads, desempenho semelhante pode ser obtido; Quando há conflitos graves de tópicos, o desempenho é muito maior que o dos CAS.
Implementação do pacote simultâneo:
Como o CAS de Java possui a semântica da memória para leitura volátil e gravação volátil, agora existem quatro maneiras de se comunicar entre os threads java:
1. A encadeamento A grava a variável volátil e, em seguida, o encadeamento B lê a variável volátil.
2. A Thread A grava a variável volátil e, em seguida, o Thread B usa CAS para atualizar a variável volátil.
3. A Thread A usa CAS para atualizar uma variável volátil e, em seguida, o Thread B usa CAS para atualizar esta variável volátil.
4. A Thread A usa CAS para atualizar uma variável volátil e, em seguida, o Thread B lê esta variável volátil.
Java's CAS uses efficient machine-level atomic instructions provided on modern processors, which perform read-change-write operations on memory atomically, which is the key to achieving synchronization in multiprocessors (essentially, a computer machine that can support atomic read-change-write instructions is an asynchronous equivalent machine that sequentially calculates Turing machines, so any modern multiprocessor will support some atomic instructions that can perform atomic Operações de leitura-mudança-gravação na memória). Ao mesmo tempo, a leitura/gravação e os CAS da variável volátil podem realizar a comunicação entre os threads. A integração desses recursos formam a pedra angular da implementação de todo o pacote simultâneo. Se analisarmos cuidadosamente a implementação do código -fonte do pacote simultâneo, encontraremos um padrão de implementação geral:
1. Primeiro, declare que a variável compartilhada é volátil;
2. Em seguida, use a atualização da condição atômica do CAS para obter a sincronização entre os threads;
3. Ao mesmo tempo, a comunicação entre os threads é alcançada usando a leitura/gravação de volátil e a semântica da memória da leitura e gravação volátil no CAS.
AQS, estruturas de dados não bloqueadoras e classes variáveis atômicas (classes no pacote java.util.concurrent.atomic), as classes básicas nesses pacotes simultâneos são implementadas usando esse padrão e as classes de alto nível no pacote concorrente dependem dessas classes básicas para implementar. De uma perspectiva geral, o diagrama de implementação do pacote simultâneo é o seguinte:
CAS (atribuição de objetos na pilha):
Java chama new object() para criar um objeto, que será alocado para o heap JVM. Então, como esse objeto é salvo na pilha?
Primeiro de tudo, quando new object() é executado, quanto espaço esse objeto precisa é realmente determinado, porque os vários tipos de dados em Java e quanto espaço eles ocupam são corrigidos (se você não estiver claro sobre seu princípio, por favor, pesquise no Google). Então, o próximo trabalho é encontrar um pedaço de espaço na pilha para armazenar esse objeto.
No caso de um único thread, geralmente existem duas estratégias de alocação:
1. Colisão do ponteiro: geralmente é aplicável à memória que é absolutamente regular (se a memória é regular depende da estratégia de reciclagem de memória). A tarefa de alocar espaço é apenas mover o ponteiro como a distância do tamanho do objeto na lateral da memória livre.
2. Lista gratuita: isso é adequado para memória não regular. Nesse caso, a JVM manterá uma lista de memória para registrar quais áreas de memória são gratuitas e qual é o tamanho. Ao alocar espaço para objetos, vá para a lista gratuita para consultar a área apropriada e alocá -la.
No entanto, é impossível para a JVM funcionar em um único estado rosqueado o tempo todo, portanto a eficiência é muito ruim. Como não é uma operação atômica ao alocar memória para outro objeto, pelo menos as seguintes etapas são necessárias: encontrar uma lista gratuita, alocar memória, modificar uma lista gratuita etc., que não é segura. Há também duas estratégias para resolver os problemas de segurança durante a simultaneidade:
1. CAS: De fato, a máquina virtual usa CAS para garantir a atomicidade da operação de atualização, deixando de tentar novamente, e o princípio é o mesmo mencionado acima.
2. TLAB: Se o CAS for usado, ele realmente terá um impacto no desempenho, portanto a JVM propôs uma estratégia de otimização mais avançada: cada thread pré-aloca um pequeno pedaço de memória na pilha Java, chamada de tampão de alocação de rosca local (TLAB). Quando o thread precisa alocar memória nele, é suficiente alocá -lo diretamente no TLAB, evitando conflitos de threads. Somente quando a memória do buffer é usada e precisar realocar a memória será executada para alocar espaço de memória maior.
Se a máquina virtual usa o TLAB pode ser configurada através -XX:+/-UseTLAB (JDK5 e versões posteriores são ativadas por padrão).
O exposto acima é todo o conteúdo deste artigo. Espero que seja útil para o aprendizado de todos e espero que todos apoiem mais o wulin.com.