Pré-requisitos para o surgimento de operações atômicas
Vamos dar uma olhada neste exemplo para ajudá-lo a entender como as operações atômicas funcionam:
public class Counter {
int count;
public void increment() {
count++;
}
}
Quando temos um thread, tudo funciona muito bem, mas se adicionarmos multithreading, obtemos resultados errados, e tudo porque a operação de incremento não é uma operação, mas três: uma solicitação para obter o valor atualcontar, em seguida, incremente-o em 1 e escreva novamente paracontar.
E quando dois threads desejam incrementar uma variável, você provavelmente perderá dados. Ou seja, ambas as threads recebem 100, como resultado, ambas escreverão 101 em vez do valor esperado de 102.
E como resolver? Você precisa usar fechaduras. A palavra-chave sincronizada ajuda a resolver esse problema, pois seu uso garante que uma thread acessará o método por vez.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
Além disso, você precisa adicionar a palavra-chave volátil , que garante a visibilidade correta das referências entre os threads. Nós revisamos seu trabalho acima.
Mas ainda há desvantagens. O maior deles é o desempenho, naquele momento em que muitos encadeamentos estão tentando adquirir um bloqueio e um obtém uma oportunidade de gravação, o restante dos encadeamentos será bloqueado ou suspenso até que o encadeamento seja liberado.
Todos esses processos, bloqueio, mudança para outro status são muito caros para o desempenho do sistema.
operações atômicas
O algoritmo usa instruções de máquina de baixo nível, como compare-and-swap (CAS, compare-and-swap, que garante a integridade dos dados e já existe uma grande quantidade de pesquisas sobre eles).
Uma operação CAS típica opera em três operandos:
- Espaço de memória para trabalho (M)
- Valor esperado existente (A) de uma variável
- Novo valor (B) a definir
O CAS atualiza atomicamente M para B, mas somente se o valor de M for o mesmo que A, caso contrário, nenhuma ação será executada.
No primeiro e no segundo caso, será devolvido o valor de M. Isso permite combinar três etapas, ou seja, obter o valor, comparar o valor e atualizá-lo. E tudo se transforma em uma operação no nível da máquina.
No momento em que uma aplicação multi-thread acessa uma variável e tenta atualizá-la e o CAS é aplicado, então uma das threads irá obtê-la e poderá atualizá-la. Mas, ao contrário dos bloqueios, outros encadeamentos simplesmente obterão erros por não serem capazes de atualizar o valor. Em seguida, eles passarão a trabalhar mais, e a troca é totalmente excluída nesse tipo de trabalho.
Nesse caso, a lógica se torna mais difícil devido ao fato de termos que lidar com a situação em que a operação CAS não funcionou com sucesso. Vamos apenas modelar o código para que ele não prossiga até que a operação seja bem-sucedida.
Introdução aos tipos atômicos
Você já se deparou com uma situação em que precisa configurar a sincronização para a variável mais simples do tipo int ?
A primeira maneira que já cobrimos é usar volátil + sincronizado . Mas também há aulas especiais de Atomic*.

Se usarmos o CAS, as operações funcionarão mais rapidamente em comparação com o primeiro método. Além disso, temos métodos especiais e muito convenientes para adicionar um valor e operações de incremento e decremento.
AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray são classes nas quais as operações são atômicas. A seguir analisaremos o trabalho com eles.
AtomicInteger
A classe AtomicInteger fornece operações em um valor int que pode ser lido e gravado atomicamente, além de fornecer operações atômicas estendidas.
Possui métodos get e set que funcionam como variáveis de leitura e escrita.
Ou seja, “acontece-antes” com qualquer recebimento subsequente da mesma variável de que falamos anteriormente. O método compareAndSet atômico também possui esses recursos de consistência de memória.
Todas as operações que retornam um novo valor são executadas atomicamente:
int addAndGet (int delta) | Adiciona um valor específico ao valor atual. |
boolean compareAndSet (int esperado, int de atualização) | Define o valor para o valor atualizado fornecido se o valor atual corresponder ao valor esperado. |
int decrementAndGet() | Diminui o valor atual em um. |
int getAndAdd(int delta) | Adiciona o valor fornecido ao valor atual. |
int getAndDecrement() | Diminui o valor atual em um. |
int getAndIncrement() | Aumenta o valor atual em um. |
int getAndSet(int newValue) | Define o valor fornecido e retorna o valor antigo. |
int incrementAndGet() | Aumenta o valor atual em um. |
lazySet(int newValue) | Por fim, defina o valor fornecido. |
booleano fracoCompareAndSet(esperado, atualização int) | Define o valor para o valor atualizado fornecido se o valor atual corresponder ao valor esperado. |
Exemplo:
ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
System.out.println(atomicInteger.get()); // prints 50