1. Propor problemas de sincronização
Suponha que usemos um processador de núcleo duplo para executar dois threads A e B, o núcleo 1 executa o thread A e o Core 2 executa o Thread B, os dois threads agora precisam adicionar 1 à variável i do membro do objeto chamado Obj. Supondo que o valor inicial de I seja 0, teoricamente, o valor de eu deveria me tornar 2 após a execução dos dois threads, mas na verdade é muito provável que o resultado seja 1.
Vamos analisar os motivos agora. Por uma questão de simplicidade de análise, não consideramos a situação do cache. De fato, existe um cache que aumentará a possibilidade de o resultado ser 1. A Thread A lê a variável I na memória na unidade de operação aritmética do kernel 1, depois executa a operação de adição e, em seguida, grava o resultado do cálculo de volta à memória. Como a operação acima não é uma operação atômica, desde que o encadeamento B lê o valor de I na memória antes de encadear a gravação do valor de I adicionando 1 de volta à memória (o valor de I é 0 no momento), então o resultado de eu definitivamente parecerá 1. Porque o valor de dois linhas a e B é 1, e o valor é 1.
A solução mais comum é usar a palavra-chave sincronizada para bloquear o objeto OBJ com o código que adiciona 1 ao código visível I em dois threads. Hoje, apresentamos uma nova solução, que deve usar classes relacionadas no pacote atômico para resolvê -la.
2. Suporte de hardware atômico
Em um único sistema de processador (Uniprocessor), as operações que podem ser concluídas em uma única instrução podem ser consideradas "operações atômicas" porque as interrupções só podem ocorrer entre as instruções (porque o agendamento de encadeamentos precisa ser concluído por meio de interrupções). Essa também é a razão pela qual alguns sistemas de instruções da CPU introduzem test_and_set, test_and_clear e outras instruções para exclusão mútua crítica de recursos. É diferente na estrutura simétrica multiprocessadora, pois vários processadores estão em execução independentemente no sistema, mesmo operações que podem ser concluídas em uma única instrução podem ser perturbadas.
Na plataforma X86, a CPU fornece os meios para travar o barramento durante a execução de instruções. Há um líder #hlockpin no chip da CPU. Se o prefixo "bloqueio" for adicionado a uma instrução no programa de linguagem de montagem, o código da máquina de montagem fará com que a CPU diminua o potencial do #HlockPin ao executar esta instrução e a liberar até o final desta instrução, travando assim o barramento. Dessa forma, outras CPUs no mesmo barramento não podem acessar a memória através do barramento por enquanto, garantindo a atomicidade desta instrução em um ambiente multiprocessador. Obviamente, nem todas as instruções podem ser prefixadas com bloqueio. Adicione apenas, o ADC e, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NÃO, OR, SBB, SUB, XOR, XADD e XCHG, as instruções podem ser prefixadas com instruções de "trava" para realizar operações atômicas.
A operação central do atômico é CAS (comparaandsset, implementada usando a instrução CMPXCHG, que é uma instrução atômica). Esta instrução possui três operandos, o valor da memória v da variável (a abreviação do valor), o valor esperado atual e da variável (a abreviação da exceção), o valor U da variável deseja atualizar (a abreviação da atualização). Quando o valor da memória é o mesmo que o valor esperado atual, o valor atualizado da variável é substituído pela variável e o pseudo-código é executado da seguinte maneira.
if (v == e) {v = u return true} else {return false}Agora usaremos as operações do CAS para resolver os problemas acima. O thread B lê a variável I na memória em uma variável temporária (assumindo que o valor lido neste momento seja 0) e, em seguida, lê o valor de I na unidade de operação aritmética do Core1. Em seguida, adiciona 1 para comparar se o valor na variável temporária é o mesmo que o valor atual de i. Se o valor de I na memória for o mesmo com o valor do resultado na unidade de operação (ou seja, i+1) (observe que esta parte é uma operação CAS, é uma operação atômica, que não pode ser interrompida e a operação do CAS em outros threads não poderá ser executada ao mesmo tempo), caso contrário, a execução de instruções falhará. Se a instrução falhar, significa que o thread a aumentou o valor de I em 1. A partir disso, podemos ver que, se o valor de eu ler por ambos os threads for 0 no início, apenas a operação CAS de um thread poderá ser bem -sucedida, porque a operação do CAS não pode ser executada simultaneamente. Para threads que não executam as operações do CAS, desde que as operações do CAS sejam executadas em looply, ele definitivamente terá sucesso. Você pode ver que não há bloqueio de threads, que é essencialmente diferente do princípio da sincronizar.
3. Introdução ao pacote atômico e análise de código -fonte
O recurso básico da classe no pacote atômico é que, em um ambiente multithread, quando vários threads operam em uma variável (incluindo tipos básicos e tipos de referência) ao mesmo tempo, é exclusiva, ou seja, quando vários threads atualizam o valor da variável ao longo do alcance, e apenas um thread pode ser bem-sucedido, e o threads é o que pode continuar a ser bem-sucedido.
Os métodos principais da classe Atomic Series chamarão vários métodos locais na classe insegura. Precisamos saber primeiro que uma coisa é a classe insegura, com seu nome completo: Sun.misc.unsfe. Esta classe contém um grande número de operações no código C, incluindo muitas alocações de memória direta e chamadas de operações atômicas. A razão pela qual está marcada como não segura é dizer que um grande número de chamadas de método nessa área terá riscos de segurança e precisará ser usado com cuidado, caso contrário, isso levará a sérias conseqüências. Por exemplo, ao alocar a memória por meio de inseguro, se você especificar certas áreas, isso pode causar alguns ponteiros como C ++ para cruzar o limite para outros processos.
As classes no pacote atômico podem ser divididas em 4 grupos de acordo com o tipo de dados operacional.
AtomicBoolean,AtomicInteger,AtomicLong
Tipos básicos de operações atômicas para threads
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
Operação atômica segura para roscas do tipo de matriz, que opera não em toda a matriz, mas em um único elemento na matriz
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
Operações seguras de threads com base em tipos básicos (inteiro longo, número inteiro e tipo de referência) em objetos de princípio de reflexão
AtomicReference,AtomicMarkableReference,AtomicStampedReference
Tipos de referência seguros para roscas e operações atômicas de tipos de referência que impedem problemas de ABA
Geralmente usamos atomicinteger, atomicreferência e referência atômica. Agora, vamos analisar o código -fonte do número inteiro atômico no pacote atômico. Os códigos de origem de outras classes são semelhantes em princípio.
1. Construtor de parâmetros
public atomicInteger (int InitialValue) {value = InitialValue;}Como pode ser visto na função do construtor, o valor é armazenado no valor variável do membro
Valor volátil privado int;
O valor variável do membro é declarado como tipo volátil, que mostra a visibilidade em vários threads, ou seja, a modificação de qualquer encadeamento será vista imediatamente em outros threads.
2.ComparanderndSet Método (o valor do valor é passado através do interno e ValueOffset)
Public final boolean Comparansset (int espera, int update) {return unsafe.compareandswapint (this, valueoffset, espera, atualização);}Este método é a operação mais central do CAS
3.GeTandSet Method, no qual o método comparaandSet é chamado
public final int getandSet (int newValue) {for (;;) {int current = get (); if (comparaandndSet (atual, newValue)) retorna corrente; }}Se outros threads alterarem o valor do valor antes de executar se (ComparaDSet (atual, newValue), o valor do valor deverá ser diferente do valor atual. Se o ComparandSet não executar, você poderá apenas obter novamente o valor do valor e continuar a comparar até que seja bem-sucedido.
4. Implementação de I ++
public final int getAndIncrement () {for (;;) {int current = get (); int próximo = corrente + 1; if (comparaandndSet (atual, próximo)) retornar corrente; }}5. Implementação de ++ i
public final int incrementAndget () {for (;;) {int current = get (); int próximo = corrente + 1; if (comparaandsset (atual, próximo)) retornar a seguir; }}4. Use AtomicInteger Exemplo
O programa a seguir usa o AtomicInteger para simular o programa de venda de ingressos. Os dois programas não venderão o mesmo ingresso no resultado em execução, nem venderão os ingressos como negativos.
pacote javaleanning; importar java.util.concurrent.atomic.atomicinteger; public classe SellTickets {AtomicInteger Tickets = new AtomicInteger (100); classe Seller implementa () runnable {@Override public void Run () {while) tmp-1)) {System.out.println (thread.currentThread (). getName ()+""+tmp);}}}}} public static void main (string [] args) {sellTickets st = sellTickets (); "Sellerb"). Start ();}}5. Problema ABA
O exemplo acima executa o resultado completamente correto. Isso se baseia no fato de que dois (ou mais) threads operam em dados na mesma direção. No exemplo acima, ambos os threads operam em tickets em diminuição. Por exemplo, se vários threads executarem operações de inscrição de objetos em uma fila compartilhada, os resultados corretos poderão ser obtidos através da classe AtomicReference (esse é realmente o caso da fila mantida no AQS). No entanto, vários threads podem ser inscritos ou excluídos, ou seja, a direção da operação dos dados é inconsistente, para que o ABA possa ocorrer.
Agora vamos dar um exemplo relativamente fácil de entender para explicar o problema da ABA. Suponha que haja dois threads T1 e T2, e esses dois threads executam operações de empilhamento e empilhamento na mesma pilha.
Usamos a cauda definida pela atômica para salvar a posição superior da pilha
Atomicreferência <t> cauda;
Supondo que o thread T1 esteja pronto para ser lançado, para empilhamento de operações, precisamos apenas atualizar a posição superior da pilha do SP para o Newsp através da operação do CAS, como mostra a Figura 1. No entanto, antes que o thread T1 execute o Tail.compareandset (SP, Newsp), o sistema executa o agendamento do thread e o thread T2 inicia a exceção. T2 executa três operações: A está fora da pilha, B está fora da pilha e, em seguida, A está na pilha. Nesse momento, o sistema começa a agendar novamente, e o thread T1 continua a executar a operação de empilhamento, mas na visão do thread T1, o elemento na parte superior da pilha ainda é um (ou seja, o T1 ainda acredita que B ainda é o próximo elemento que a pilha não é alterada, e a situação real é que a figura 2. O ponteiro superior da pilha é apontado para o nó B. Na verdade, B não existe mais na pilha. O resultado depois que o T1 coloca uma fora da pilha é mostrado na Figura 3, o que obviamente não é o resultado correto.
6. Soluções para problemas de ABA
Use atomicmarkableReference, atomicstampedReference. Use as duas classes atômicas mencionadas acima para executar operações. Ao implementar a instrução ComparaDSet, eles não apenas precisam comparar o valor anterior e o valor esperado do objeto, mas também precisam comparar o valor atual do carimbo (operação) e o valor esperado (operação) do carimbo. Somente quando tudo isso é verdadeiro, o método ComparaDSet pode ter sucesso. Cada vez que a atualização é bem -sucedida, o valor do carimbo muda e a configuração do valor do carimbo é controlada pelo próprio programador.
Public Boolean ComparaDSet (v Espera -Referência, v newReference, int esperastamp, int newstamp) {par <V> current = par; retorna Esperme Reference == current.Reference && whrenderstamp == current.stamp && (newReference == Current.Reference & & Newstamp == Current.stamp) || CASPAIR (CURCAL,Nesse momento, o método ComparaDSET requer quatro parâmetros: Referência Espermediária, NewReference, WerendStamp, Newstamp. Quando usamos esse método, devemos garantir que o valor esperado do carimbo não seja o mesmo que o valor do carimbo de atualização. Normalmente newstamp = esperadostamp+1
Pegue os exemplos acima
Suponha que o encadeamento T1 esteja antes da pilha: o SP aponta para A e o valor do carimbo é 100.
O thread T2 é executado: depois que A é liberado, o SP aponta para B e o valor do carimbo se torna 101.
Depois que B é liberado, o SP aponta para C e o valor do carimbo se torna 102.
Depois que A é colocado na pilha, o SP aponta para A e o valor do carimbo se torna 103.
O Thread T1 continua a executar a instrução ComparaDSet e constata que, embora o SP ainda aponte para A, o valor esperado do valor do carimbo 100 é diferente do valor atual 103. Portanto, o comparaandset falha. Você precisa obter o valor do Newsp (neste momento, o Newsp apontará para C) e o valor esperado do valor do carimbo 103 e, em seguida, executará a operação ComparaDSet novamente. Dessa forma, um lançamento com sucesso da pilha, SP apontará para C.
Observe que, como a ComparaDSet pode alterar apenas um valor por vez e não pode alterar o NewReference e o NewStamp ao mesmo tempo, durante a implementação, uma classe de pares é definida internamente para transformar o NewReference e o Newstamp em um objeto. Ao executar operações do CAS, na verdade é uma operação no objeto par.
Classe estática privada par <t> {referência final t; Final Int Stamp; par privado (referência t, int selo) {this.reference = reference; this.stamp = carimbo; } estático <t> par <t> de (t referência, int carim) {return new par <t> (referência, carimbo); }}Para atomicmarkableReference, o valor do carimbo é uma variável booleana, enquanto o valor do carimbo no atomicstampedReference é uma variável inteira.
Resumir
O exposto acima é tudo sobre a breve discussão deste artigo sobre os princípios de implementação e aplicações de pacotes atômicos em Java. 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.