Atômicos e operações lock-free
TL;DR
O pacote
java.util.concurrent.atomicoferece variáveis que podem ser lidas e escritas atomicamente sem locks, usando a instrução de hardware CAS (compare-and-swap).AtomicInteger,AtomicLongeAtomicReferencesão os tipos mais usados;getAndUpdate/updateAndGet/accumulateAndGetpermitem transformações arbitrárias em loop CAS. Sob alta contenção,LongAddereDoubleAdderescalam melhor queAtomicLongporque distribuem os incrementos em células (striping) e somam sob demanda — massum()não é um snapshot atômico.AtomicStampedReferenceanexa um inteiro de versão ao valor para contornar o clássico problema ABA, onde CAS aceita indevidamente um valor porque ele voltou ao estado original após ser modificado.
O que é
Operações atômicas são operações indivisíveis do ponto de vista de outras threads: ou todo o efeito é visível, ou nenhum é. Em Java, o mecanismo primitivo para isso, abaixo dos synchronized/Lock, é a instrução de CPU chamada compare-and-swap (CAS): em uma única operação de hardware, o processador lê um valor, compara com um esperado e, somente se forem iguais, substitui pelo novo valor — tudo sem que outra thread possa intervir no meio.
O pacote java.util.concurrent.atomic (introduzido no Java 5) expõe esse mecanismo com uma API de alto nível. As classes não usam bloqueio de sistema operacional; por isso chamamos essas estruturas de lock-free — threads não se bloqueiam mutuamente, apenas repetem a tentativa até que o CAS tenha sucesso.
A distinção prática em relação a volatile:
| Propriedade | volatile | AtomicLong |
|---|---|---|
| Visibilidade | Sim | Sim |
| Ordenação | Sim | Sim |
| Atomicidade de operações compostas | Não | Sim |
volatile int counter; counter++; envolve três operações separadas (leitura, incremento, escrita) — não é atômico. AtomicInteger.incrementAndGet() é uma única operação CAS.
Por que importa
Contadores, geradores de sequência, flags e referências compartilhadas são onipresentes em código concorrente. Protegê-los com synchronized funciona, mas introduz contenção: threads bloqueiam, aguardam o lock ser liberado e sofrem trocas de contexto de sistema operacional. Sob carga alta, isso se torna gargalo mensurável.
Atômicos permitem um trade-off diferente: threads nunca bloqueiam, apenas repetem a tentativa (loop CAS) quando há conflito. Para operações de curta duração como incremento de contador, o custo de uma segunda tentativa é ordens de grandeza menor que um context-switch. O resultado prático:
- Contadores de métricas: substituir
synchronized intporAtomicLongouLongAdderpode reduzir latência de p99 em aplicações de alto throughput. - Geradores de ID sequencial: CAS garante unicidade sem lock global.
- Algoritmos lock-free: estruturas como
ConcurrentLinkedQueuee partes internas deConcurrentHashMapsão construídas sobre CAS.
Em entrevistas sênior, conhecer atômicos demonstra domínio da camada entre synchronized e código puramente reativo — e a capacidade de escolher a ferramenta certa para o nível de contenção esperado.
Como funciona
AtomicInteger / AtomicLong / AtomicReference
As classes fundamentais do pacote para tipos primitivos e referências de objetos:
AtomicInteger ai = new AtomicInteger(0);
int old = ai.getAndIncrement(); // retorna valor anterior, depois incrementa
int now = ai.incrementAndGet(); // incrementa, depois retorna novo valor
int prev = ai.getAndAdd(5); // retorna anterior e soma 5
// Leitura / escrita diretas (semântica volatile)
int val = ai.get();
ai.set(42);
ai.lazySet(42); // sem barreira total — mais rápido, adequado quando não há leituras imediatas
AtomicLong al = new AtomicLong(0L);
al.incrementAndGet(); // idêntico ao AtomicInteger, mas para long
AtomicReference<String> ref = new AtomicReference<>("inicial");
String anterior = ref.getAndSet("novo"); // troca e retorna o anteriorAtomicReference permite que qualquer referência de objeto seja trocada atomicamente, o que é útil para substituir snapshots imutáveis de estruturas de dados sem lock.
CAS (compare-and-swap) e compareAndSet
compareAndSet é a operação central de todo o pacote:
AtomicInteger ai = new AtomicInteger(10);
// Troca 10 → 20 somente se o valor atual for 10
boolean sucesso = ai.compareAndSet(10, 20); // retorna true
boolean falha = ai.compareAndSet(10, 30); // retorna false (valor já é 20)Internamente, compareAndSet mapeia para a instrução CMPXCHG (x86) ou equivalente em outras arquiteturas — uma única instrução que executa leitura, comparação e escrita de forma atômica no nível de hardware, garantindo que nenhum outro core interrompa a operação no meio.
A semântica de memória é a de volatile: a escrita CAS é uma release e a leitura CAS é uma acquire, garantindo que efeitos anteriores na thread escritora sejam visíveis para threads que leem o novo valor.
Padrão de loop CAS para operações arbitrárias:
AtomicLong ref = new AtomicLong(0);
// "Dobra o valor atual atomicamente" — operação que pode falhar e precisar de retry
long prev, updated;
do {
prev = ref.get();
updated = prev * 2;
} while (!ref.compareAndSet(prev, updated));
// Resultado: não determinístico quanto ao número de tentativas sob contenção,
// mas sempre correto — nunca aplica o dobro ao valor errado.Esse padrão é seguro: o loop garante que, se outra thread modificou o valor entre o get() e o compareAndSet(), a tentativa falha e recomeça com o valor atual.
getAndUpdate / updateAndGet / accumulateAndGet
Os métodos funcionais encapsulam o loop CAS, tornando o código mais expressivo:
AtomicInteger counter = new AtomicInteger(5);
// updateAndGet: aplica função e retorna novo valor
int novo = counter.updateAndGet(x -> x * 2); // 10
// getAndUpdate: aplica função e retorna valor anterior
int antes = counter.getAndUpdate(x -> x + 1); // 10 (retorna antes de somar)
// accumulateAndGet: combina valor atual com um delta usando função binária
int total = counter.accumulateAndGet(3, Integer::sum); // atual + 3
// Equivalente com AtomicLong
AtomicLong al = new AtomicLong(100L);
long resultado = al.updateAndGet(v -> v > 0 ? v - 1 : 0); // decremento com chão em 0A função passada deve ser sem efeitos colaterais (side-effect-free) porque pode ser re-executada múltiplas vezes se o CAS falhar sob contenção. Não use lambdas que façam I/O, modifiquem estado externo ou dependam de ordem de execução.
LongAdder / DoubleAdder (striping sob alta contenção)
LongAdder e DoubleAdder, introduzidos no Java 8, resolvem um problema específico: quando muitas threads incrementam simultaneamente um único AtomicLong, todas competem pelo mesmo valor, aumentando as falhas de CAS e degradando o throughput.
A solução é striping: internamente, LongAdder mantém uma célula base e um array de células adicionais. Sob contenção, cada thread tende a atualizar sua própria célula; sum() percorre todas as células e soma:
LongAdder contador = new LongAdder();
// Threads incrementando em paralelo — cada uma provavelmente acessa célula diferente
contador.increment(); // equivale a add(1)
contador.decrement(); // equivale a add(-1)
contador.add(5L);
long total = contador.sum(); // soma todas as células internas
// ATENÇÃO: sum() não é um snapshot atômico — veja Armadilhas
contador.reset(); // zera todas as células — RISCO: apenas em quiescência
long totalEReset = contador.sumThenReset(); // sum + reset — também racyQuando usar LongAdder vs AtomicLong:
| Cenário | Recomendação |
|---|---|
| Contador de métricas, muitas threads escrevendo, leituras ocasionais | LongAdder |
| Gerador de sequência único (cada valor importa) | AtomicLong |
| Baixa contenção (poucas threads) | Indiferente; AtomicLong é mais simples |
Precisa de compareAndSet | Somente AtomicLong — LongAdder não expõe CAS |
DoubleAdder é o equivalente para double, com as mesmas garantias (e as mesmas ressalvas de sum()).
AtomicStampedReference e o problema ABA
O problema ABA ocorre quando uma thread lê o valor A, é preemptada, e outra thread modifica o valor para B e depois volta para A. Quando a primeira thread executa o CAS, ele enxerga A e tem sucesso — mas o valor passou por uma modificação intermediária que pode deixar o sistema em estado inválido.
Exemplo clássico em estruturas de dados:
// Thread 1: lê nó A no topo da pilha lock-free
// Thread 2: remove A, insere B, remove B, reusa o objeto A e o recoloca
// Thread 1: CAS de A → C tem sucesso, mas o estado da pilha está erradoAtomicStampedReference resolve anexando um inteiro de versão (stamp) ao valor. O CAS só tem sucesso se tanto o valor quanto o stamp conferem:
// Estado inicial: referência "nó-A", stamp 0
AtomicStampedReference<String> asr = new AtomicStampedReference<>("nó-A", 0);
// Leitura segura: pegar referência e stamp juntos
int[] stampHolder = new int[1];
String atual = asr.get(stampHolder); // atual = "nó-A", stampHolder[0] = 0
// CAS que requer tanto referência quanto stamp corretos
boolean ok = asr.compareAndSet(
"nó-A", "nó-C", // referência esperada → nova
0, 1 // stamp esperado → novo
);
// Se outra thread já incrementou o stamp (mesmo que voltou para "nó-A"),
// este CAS falha — o ABA é detectado.AtomicMarkableReference é uma variação que usa um boolean em vez de um inteiro, adequada quando só é preciso distinguir “original” de “modificado uma vez”.
Na prática
Contador lock-free com AtomicLong
O caso mais direto: substituir um campo int/long sincronizado por AtomicLong:
public class PageViewCounter {
private final AtomicLong views = new AtomicLong(0L);
// Chamado por múltiplas threads ao mesmo tempo
public void record() {
views.incrementAndGet();
}
public long total() {
return views.get();
}
// Snapshot periódico para métricas — zera e retorna
public long drainAndReset() {
return views.getAndSet(0L);
}
}drainAndReset é atômico: mesmo que outra thread incremente entre getAndSet e a próxima chamada, nenhum incremento é perdido — apenas pode cair no próximo período de coleta.
Gerador de sequência com loop CAS
Quando a semântica exige controle sobre o valor gerado (por exemplo, garantir que IDs nunca regridam, mesmo sob concorrência):
public class StrictSequenceGenerator {
private final AtomicLong current = new AtomicLong(0L);
/**
* Retorna o próximo valor maior que o atual.
* Garante monotonia estrita mesmo com múltiplas threads.
* Não determinístico quanto ao número de tentativas — pode fazer retry sob contenção.
*/
public long next(long minimum) {
long prev, next;
do {
prev = current.get();
next = Math.max(prev + 1, minimum);
} while (!current.compareAndSet(prev, next));
return next;
}
}O loop não tem limite fixo de tentativas — sob contenção extrema pode iterar mais vezes. Para casos onde isso é inaceitável, considere um lock com timeout (ver 05 - Locks explícitos).
Métrica de alto volume com LongAdder
Em sistemas de telemetria onde dezenas de threads reportam eventos por segundo:
public class RequestMetrics {
private final LongAdder totalRequests = new LongAdder();
private final LongAdder totalErrors = new LongAdder();
private final LongAdder totalLatencyMs = new LongAdder();
public void record(long latencyMs, boolean error) {
totalRequests.increment();
totalLatencyMs.add(latencyMs);
if (error) totalErrors.increment();
}
// Chamado periodicamente pelo coletor de métricas — não durante pico de escrita
public Snapshot snapshot() {
return new Snapshot(
totalRequests.sum(),
totalErrors.sum(),
totalLatencyMs.sum()
);
// Nota: os três sum() não são lidos atomicamente como grupo.
// Para snapshots totalmente consistentes, prefira LongAccumulator com lock externo.
}
}LongAdder aqui escala sob carga porque cada thread que chega em colisão recebe uma célula diferente — o gargalo de CAS é distribuído horizontalmente.
Armadilhas
(1) Loop CAS sem progressão garantida sob contenção extrema
O problema: updateAndGet e qualquer loop CAS manual podem repetir indefinidamente quando muitas threads competem pelo mesmo valor. Tecnicamente não é deadlock (threads não se bloqueiam), mas na prática o comportamento se assemelha a livelock: threads consomem CPU sem progredir.
// RUIM para contenção muito alta: todas as threads repetem o CAS em loop
AtomicLong shared = new AtomicLong(0L);
// 1000 threads fazendo isso ao mesmo tempo → CAS falha constantemente
long resultado = shared.updateAndGet(x -> compute(x));// FIX 1: se só precisa de contagem, use LongAdder — sem CAS conflitante
LongAdder adder = new LongAdder();
adder.add(delta); // nenhum retry; células absorvem a contenção
// FIX 2: se precisa de CAS por semântica, introduza backoff com jitter
long prev, next;
int retries = 0;
do {
prev = shared.get();
next = computeNext(prev);
if (retries++ > 10) {
LockSupport.parkNanos(ThreadLocalRandom.current().nextLong(1_000, 10_000));
}
} while (!shared.compareAndSet(prev, next));A escolha entre atômicos e ReentrantLock depende do perfil de contenção: baixa-média contenção favorece atômicos; alta contenção sustentada pode favorecer um lock com fila de espera.
(2) Problema ABA com AtomicReference simples
O problema: usar AtomicReference.compareAndSet em estruturas de dados com reuso de objetos (como pools de nós em pilhas lock-free) pode aceitar um CAS indevidamente porque o valor voltou para a referência original.
// RUIM: AtomicReference não detecta ciclos A→B→A
AtomicReference<Node> top = new AtomicReference<>(nodeA);
// Thread 2, enquanto Thread 1 estava preemptada:
// top: A → B → A (com nodeA reusado do pool)
// Thread 1 executa o CAS com expectedRef = nodeA → SUCESSO indevido
// Resultado: estrutura da pilha corrompida, nó B "perdido"
top.compareAndSet(nodeA, nodeC); // passa, mas não deveria// FIX: AtomicStampedReference com versão incremental
AtomicStampedReference<Node> top = new AtomicStampedReference<>(nodeA, 0);
int[] stamp = new int[1];
Node current = top.get(stamp);
// CAS agora exige que o stamp também corresponda
top.compareAndSet(current, nodeC, stamp[0], stamp[0] + 1);
// Se Thread 2 incrementou o stamp (mesmo que restaurou a referência),
// este CAS falha corretamente.Na prática, algoritmos lock-free corretos para pilhas e filas (como os de Maged Michael e Michael Scott) sempre incorporam proteção ABA — ou via stamp, ou via garbage collection (em Java, GC impede reuso exato do mesmo endereço de memória na maioria dos casos, reduzindo o risco, mas não eliminando-o em sistemas com pool de objetos).
(3) Assumir que LongAdder.sum() é consistente durante updates
O problema: sum() percorre células internas somando-as uma por uma. Durante essa travessia, outras threads podem estar modificando células já visitadas ou ainda não visitadas. O Javadoc do Oracle é explícito: o valor retornado não é um snapshot atômico; atualizações concorrentes em andamento podem não ser refletidas.
// RUIM: assumir que sum() captura estado exato de um instante
LongAdder requests = new LongAdder();
// Thread de monitoramento:
long total = requests.sum(); // pode estar "desatualizado" por alguns nanos
log.info("Requests exatos agora: {}", total); // premissa falsa// FIX 1: aceite a semântica aproximada — para métricas de observabilidade é suficiente
// "~N requests no último período" é tão útil quanto "exatamente N"
// FIX 2: se precisar de consistência exata entre múltiplas variáveis,
// use um lock externo para o snapshot:
synchronized (metricsLock) {
long r = requests.sum();
long e = errors.sum();
long l = latencySum.sum();
return new Snapshot(r, e, l);
}
// FIX 3: zerar e ler são operações separadas — reset() é racy se há threads ativas
// NUNCA:
long snapshot = counter.sum();
counter.reset(); // threads podem incrementar entre sum() e reset(), perdendo eventos
// USE sumThenReset() com cautela — mesmo ela não é atômica com o resto do sistema,
// mas pelo menos reduz a janela de inconsistência entre soma e zeraEm entrevista
Frase pronta (inglês)
“Atomic classes in
java.util.concurrent.atomicprovide lock-free thread safety by leveraging hardware CAS — compare-and-swap — instructions, which atomically read, compare, and conditionally write a value in a single CPU operation, without acquiring an OS-level lock.” “For simple counters and references,AtomicLongandAtomicReferenceare the go-to tools;compareAndSetis the primitive, andupdateAndGet/accumulateAndGetwrap the CAS loop for you — but the function you pass must be side-effect-free because it may run multiple times under contention.” “When thread density is high and most operations are plain increments — think metrics collection or hit counters —LongAdderoutperformsAtomicLongbecause it uses striping: each thread updates its own cell, andsum()aggregates them on demand; the trade-off is thatsum()is not an atomic snapshot, which is fine for approximate instrumentation but wrong for sequence generators or anything requiring strict consistency.”
Vocabulário
| Termo PT | Termo EN |
|---|---|
| operação atômica | atomic operation |
| comparação e troca | compare-and-swap (CAS) |
| sem bloqueio | lock-free |
| problema ABA | ABA problem |
| referência estampada | stamped reference |
| distribuição de células | cell striping |
| snapshot atômico | atomic snapshot |
| laço de retry | CAS retry loop |
| memória volátil | volatile memory semantics |
| contenção de threads | thread contention |
| gerador de sequência | sequence generator |
| versão de carimbo | version stamp |
Veja também
- 05 - Locks explícitos
- 07 - Concurrent collections
- 11 - Java Memory Model em profundidade
- MOC do galho
- Trilha Java
- Java Concurrency
- atomic
- CAS
- ABA problem
- lock-free
Referências
- java.util.concurrent.atomic — Package Summary, Java 21 (Oracle)
- AtomicInteger — Java 21 API (Oracle)
- AtomicLong — Java 21 API (Oracle)
- LongAdder — Java 21 API (Oracle)
- AtomicStampedReference — Java 21 API (Oracle)
- Brian Goetz et al. Java Concurrency in Practice. Addison-Wesley, 2006. Cap. 15 — “Atomic Variables and Nonblocking Synchronization”.