Atomicidade e Atomics: Segurança entre Threads sem Bloqueios em Java
Em aplicações multithread, um dos problemas mais comuns é a condição de corrida (race condition), que ocorre quando vários threads tentam ler e modificar a mesma variável ao mesmo tempo. Isso leva a resultados imprevisíveis e erros difíceis de reproduzir.
Considere o exemplo clássico de um contador compartilhado:
public class Program {
public static void main(String[] args) throws InterruptedException {
int numThreads = 1000;
int incrementsPerThread = 1000;
Counter counter = new Counter();
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
counter.increment();
}
});
threads[i].start();
}
Thread.sleep(1000);
for (Thread t : threads) {
t.join();
}
System.out.println("Counter: " + counter.getCounter());
}
}
class Counter {
private volatile long counter = 0;
long getCounter() { return counter; }
void increment() { counter++; } // NÃO É ATOMÁRICO!
}Mesmo com o uso de volatile, o resultado será inconsistente — algo como:
Counter: 944695
ou
Counter: 870973
O valor nunca será exatamente 1.000.000, como esperado.
Isso acontece porque a operação counter++ não é atômica: ela é composta de três etapas — leitura, incremento e escrita — que podem ser intercaladas por outros threads.
Por que não usar bloqueios (synchronized / ReentrantLock)?
Uma solução seria sincronizar o método increment(), por exemplo com synchronized.
Mas isso introduz bloqueios, e enquanto um thread segura o lock, os outros ficam bloqueados, esperando.
Em sistemas com alta concorrência, isso gera espera, trocas de contexto e queda de desempenho.
Solução eficiente: pacote java.util.concurrent.atomic
Para resolver esse tipo de problema sem bloquear threads, o Java oferece o pacote java.util.concurrent.atomic.
Ele contém classes como:
AtomicIntegerAtomicLongAtomicBooleanAtomicReference
Essas classes usam instruções atômicas de hardware (como compare-and-swap) para garantir consistência sem bloqueios.
As operações são thread-safe e não bloqueantes, tornando o código muito mais eficiente.
Exemplo com AtomicLong
import java.util.concurrent.atomic.AtomicLong;
public class Program {
public static void main(String[] args) throws InterruptedException {
int numThreads = 1000;
int incrementsPerThread = 1000;
Counter counter = new Counter();
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
counter.increment();
}
});
threads[i].start();
}
Thread.sleep(1000);
for (Thread t : threads) {
t.join();
}
System.out.println("Counter: " + counter.getCounter());
}
}
class Counter {
private AtomicLong counter = new AtomicLong(0);
long getCounter() { return counter.get(); }
void increment() { counter.getAndIncrement(); } // OPERAÇÃO ATÔMICA!
}Saída determinística:
Counter: 1000000
Agora o resultado está correto, pois getAndIncrement() é uma operação atômica — executada integralmente, sem interferência de outros threads.
Principais métodos de AtomicLong
| Método | Descrição |
|---|---|
get() | Retorna o valor atual |
set(long newValue) | Define um novo valor |
incrementAndGet() | Incrementa e retorna o novo valor (++i) |
getAndIncrement() | Incrementa e retorna o valor anterior (i++) |
decrementAndGet() | Decrementa e retorna o novo valor (--i) |
getAndDecrement() | Decrementa e retorna o valor anterior (i--) |
addAndGet(long delta) | Soma um valor e retorna o novo |
getAndAdd(long delta) | Soma um valor e retorna o antigo |
compareAndSet(long expect, long update) | Altera o valor apenas se ele for igual ao esperado (CAS) |
Essas operações são lock-free, ou seja, garantem integridade sem usar synchronized.
LongAdder: desempenho otimizado para alta concorrência
Em ambientes com muitos threads atualizando o mesmo contador, até as operações atômicas podem gerar contenção (disputa pelo mesmo valor).
Para isso, o Java introduziu o LongAdder, que distribui a carga entre múltiplas células internas.
Cada thread atualiza sua própria célula, e o método sum() combina os valores no final.
Isso reduz drasticamente a contenção e melhora o desempenho em sistemas de alta concorrência.
import java.util.concurrent.atomic.LongAdder;
public class Program {
public static void main(String[] args) throws InterruptedException {
int numThreads = 1000;
int incrementsPerThread = 1000;
Counter counter = new Counter();
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
counter.increment();
}
});
threads[i].start();
}
Thread.sleep(1000);
for (Thread t : threads) {
t.join();
}
System.out.println("Counter: " + counter.getCounter());
}
}
class Counter {
private LongAdder counter = new LongAdder();
long getCounter() { return counter.longValue(); }
void increment() { counter.increment(); } // cada thread atualiza sua célula
}Saída:
Counter: 1000000
O LongAdder é ideal para contadores de métricas e estatísticas, onde não é necessário conhecer o valor exato a cada momento, apenas o total agregado ao final.
Resumo
| Classe | Características | Uso típico |
|---|---|---|
volatile | Garante visibilidade, mas não atomicidade | Sinalizadores simples (running, active) |
AtomicLong | Opera de forma atômica e thread-safe, mas pode sofrer contenção | Contadores moderados |
LongAdder | Divide o estado entre múltiplas células para reduzir contenção | Contadores de alta concorrência |
Conclusão
- As operações
++e--não são atômicas — mesmo comvolatile. - Use
AtomicLongpara contadores simples, quando muitos threads compartilham o mesmo valor. - Use
LongAdderquando há muitos threads atualizando o mesmo contador simultaneamente. - Essas soluções eliminam a necessidade de bloqueios (
synchronized), mantendo o código seguro e escalável.