Variáveis Atômicas em Java
Operações thread-safe sem bloqueio em Java com as classes java.util.concurrent.atomic — contadores, referências e compare-and-set.
volatile torna uma única leitura ou escrita thread-safe. Ele não consegue tornar counter++ thread-safe — isso são três operações. O pacote java.util.concurrent.atomic preenche essa lacuna. Suas classes encapsulam um único valor e expõem operações como increment-and-get e compare-and-set como instruções atômicas únicas — sem lock, sem bloco synchronized, apenas uma primitiva de CPU (compare-and-swap, ou CAS) que a JVM compila diretamente.
As variáveis atômicas são a ferramenta certa para um número surpreendentemente grande de padrões multithread: contadores, números de sequência, estado semelhante a flag e qualquer idioma de "publicar um novo snapshot imutável". São mais rápidas que synchronized sob contenção e dramaticamente mais simples do que criar manualmente um lock em torno de um único campo.
A família
O pacote tem oito classes comumente usadas:
| Classe | Encapsula | Operações comuns |
|---|---|---|
AtomicInteger | int | get, set, incrementAndGet, addAndGet, compareAndSet |
AtomicLong | long | as mesmas acima, em long |
AtomicBoolean | boolean | get, set, compareAndSet |
AtomicReference<V> | V (qualquer referência de objeto) | get, set, compareAndSet, updateAndGet |
AtomicIntegerArray | int[] | operações atômicas por índice |
AtomicLongArray | long[] | operações atômicas por índice |
AtomicReferenceArray<V> | V[] | operações atômicas por índice |
LongAdder / LongAccumulator | long | contador de alta contenção |
As primeiras quatro são as que você vai usar em 99% das vezes.
AtomicInteger — o contador correto
O substituto para "volatile int mais ++":
AtomicInteger counter = new AtomicInteger(); // starts at 0
counter.incrementAndGet(); // ++counter, atomic
counter.getAndIncrement(); // counter++, atomic
counter.addAndGet(5); // counter += 5, atomic
counter.set(42); // counter = 42, atomic
int n = counter.get(); // read
counter.compareAndSet(42, 100); // if (counter == 42) counter = 100; return whether it changedincrementAndGet é o que você quer para um contador simples; internamente é um loop CAS que a CPU executa em uma instrução nos sistemas x86 modernos (LOCK XADD). Toda a operação é uma única transação de memória em nível de barramento — muito mais barata do que adquirir até mesmo um lock synchronized sem contenção.
compareAndSet(expected, new) é o bloco de construção para quase tudo mais. Ele escreve atomicamente new somente se o valor atual for expected, e retorna se a escrita ocorreu. Com ele você pode construir qualquer atualização atômica de campo único:
AtomicInteger max = new AtomicInteger(Integer.MIN_VALUE);
void recordMax(int v) {
int cur;
do {
cur = max.get();
if (v <= cur) return; // nothing to do
} while (!max.compareAndSet(cur, v)); // retry if someone else updated
}O loop CAS é o padrão padrão: leia, compute, tente escrever, repita em caso de conflito. É como incrementAndGet é implementado; é como você escreveria qualquer atualização composta em um único campo.
O Java 8 simplificou o loop:
max.updateAndGet(cur -> Math.max(cur, v)); // CAS loop hiddenupdateAndGet, accumulateAndGet e getAndUpdate recebem uma função e executam o loop CAS para você. Prefira-os quando se encaixarem.
AtomicReference<V> — a forma correta de trocar um objeto
Quando o estado compartilhado é mais do que uma primitiva — um mapa de configuração, um snapshot em cache, um holder imutável — AtomicReference permite trocar atomicamente o objeto inteiro:
AtomicReference<Config> currentConfig = new AtomicReference<>(initialConfig);
void reload() {
Config c = readConfigFromDisk(); // expensive, lock-free
currentConfig.set(c); // publish atomically
}
Config get() { return currentConfig.get(); }O truque: o conteúdo de Config deve ser imutável (ou não tocado após a publicação). A troca atômica publica um valor finalizado; se outras threads então mutarem os internos do valor, você perdeu a segurança. Este é o padrão de snapshot imutável, e é como a maioria dos caches concorrentes, tabelas de rotas e objetos de "configuração global" são construídos.
updateAndGet em uma referência também é extremamente útil:
AtomicReference<List<String>> log = new AtomicReference<>(List.of());
void append(String line) {
log.updateAndGet(old -> {
var copy = new ArrayList<>(old);
copy.add(line);
return List.copyOf(copy); // immutable snapshot
});
}Cada leitor obtém uma lista imutável consistente. Os escritores competem; o loop CAS repete os poucos que perdem a disputa. Barato sob baixa contenção, lento mas correto sob alta contenção.
LongAdder — o contador de alta contenção
Sob contenção pesada, AtomicLong.incrementAndGet torna-se um gargalo — cada thread está martelando o mesmo endereço de memória, e a CPU tem que serializar as transações de barramento. LongAdder resolve isso mantendo vários contadores internos, um por CPU, e somando-os na leitura:
LongAdder requestCount = new LongAdder();
void onRequest() { requestCount.increment(); } // append-only, no contention
long snapshot() { return requestCount.sum(); } // sums every cell — not atomic but eventually consistentUse LongAdder quando:
- O contador é incrementado a partir de muitas threads simultaneamente (pense: métricas por requisição em um servidor web).
- Você o lê raramente (a cada poucos segundos para um painel).
Use AtomicLong quando:
- O incremento é raro ou em thread única.
- Você precisa de uma leitura precisa e instantânea.
LongAdder é um dos contadores concorrentes mais rápidos disponíveis — mas a troca é que sum() não é atômico com incrementos concorrentes. Para o caso típico de relatório de métricas, isso é aceitável.
O que as variáveis atômicas não são
As variáveis atômicas se limitam a um campo. Elas não se compõem entre múltiplos campos:
AtomicInteger a = new AtomicInteger();
AtomicInteger b = new AtomicInteger();
a.incrementAndGet(); // atomic on its own
b.incrementAndGet(); // atomic on its own
// but the pair is NOT atomic — another thread can see new a, old bSe seu invariante abrange múltiplos campos ("a == b + 1 sempre"), você precisa de um lock (ou de um único atômico em um objeto holder contendo ambos).
As variáveis atômicas também não ajudam com a visibilidade de campos não relacionados. Escrever em um atômico não publica outros campos da forma que volatile faz. Torne esses outros campos volatile (ou final, ou escreva-os através do atômico).
compareAndExchange e a nova API (Java 9+)
O Java 9 adicionou compareAndExchange (retorna o valor atual, não apenas um boolean):
int prev = counter.compareAndExchange(expected, newVal);
if (prev == expected) { // we won
...
} else { // somebody else got there first
// prev is the actual current value
}O Java 9 também adicionou a API VarHandle que expõe CAS fraco, acesso ordenado, etc., para bibliotecas concorrentes de baixo nível. Você raramente precisará dela; menciona-se aqui para que você conheça o nome.
Um exemplo prático: contador e snapshot
O programa abaixo contrasta quatro contadores: não sincronizado, volatile, AtomicInteger e LongAdder. Todos os quatro são atingidos por 8 threads que fazem 100.000 incrementos cada.
O que observar na execução:
plainevolatileambos perderam atualizações — às vezes de forma espetacular (uma contagem final muito abaixo do esperado800.000).volatilecorrige o problema de visibilidade, masn++ainda são três operações. Esta é a coisa mais importante a lembrar sobrevolatile: ele não torna atualizações compostas atômicas.AtomicIntegerproduziu a contagem esperada exata, em toda execução. O custo por incremento foi de alguns nanossegundos — significativamente maior quen++em umintsimples (que é um ou dois), mas sem aquisições de lock e sem bloqueio de thread. Sob contenção, é mais rápido quesynchronizedpor uma larga margem.LongAdderfoi o contador mais rápido sob a carga de 8 threads — ele distribui escritas em células separadas por CPU, então as threads não competem em uma única linha de cache. A troca é quesum()não é atômico comincrement()(um leitor pode ver um total levemente desatualizado), o que é exatamente a troca certa para métricas e contadores onde precisão instantânea não importa.- O max do loop CAS registrou o maior valor visto em todas as amostras. O loop é o padrão geral: leia o valor atual, compute o novo valor desejado, tente escrevê-lo; se alguém escreveu primeiro, o CAS falha e você repete. A maioria das chamadas
updateAndGeteaccumulateAndGetsão este loop com o código repetitivo oculto. - O
AtomicReference<List<String>>produziu um snapshot imutável do log. Cada escritor construiu uma nova cópia imutável e tentou publicá-la; sob contenção, dois escritores podem construir uma cópia e o CAS de um deles falha — esse thread repete, lê a lista recém-atualizada e mescla. O padrão é dispendioso sob contenção pesada (muitas cópias descartadas) mas ideal para snapshots de "leitura intensa, reconstrução ocasional".
O que vem a seguir
O próximo capítulo, Java Locks, começa a história do java.util.concurrent.locks — a interface Lock, por que ela existe ao lado de synchronized, e as capacidades (tryLock, lockInterruptibly, Condition) que ela adiciona e que o monitor intrínseco não possui.