W3docs

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:

ClasseEncapsulaOperações comuns
AtomicIntegerintget, set, incrementAndGet, addAndGet, compareAndSet
AtomicLonglongas mesmas acima, em long
AtomicBooleanbooleanget, set, compareAndSet
AtomicReference<V>V (qualquer referência de objeto)get, set, compareAndSet, updateAndGet
AtomicIntegerArrayint[]operações atômicas por índice
AtomicLongArraylong[]operações atômicas por índice
AtomicReferenceArray<V>V[]operações atômicas por índice
LongAdder / LongAccumulatorlongcontador 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 changed

incrementAndGet é 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 hidden

updateAndGet, 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 consistent

Use 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 b

Se 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.

java— editable, runs on the server

O que observar na execução:

  • plain e volatile ambos perderam atualizações — às vezes de forma espetacular (uma contagem final muito abaixo do esperado 800.000). volatile corrige o problema de visibilidade, mas n++ ainda são três operações. Esta é a coisa mais importante a lembrar sobre volatile: ele não torna atualizações compostas atômicas.
  • AtomicInteger produziu a contagem esperada exata, em toda execução. O custo por incremento foi de alguns nanossegundos — significativamente maior que n++ em um int simples (que é um ou dois), mas sem aquisições de lock e sem bloqueio de thread. Sob contenção, é mais rápido que synchronized por uma larga margem.
  • LongAdder foi 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 é que sum() não é atômico com increment() (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 updateAndGet e accumulateAndGet sã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.

Prática

Prática
Qual destas é a forma correta de incrementar com segurança um contador compartilhado a partir de muitas threads em um loop apertado?
Qual destas é a forma correta de incrementar com segurança um contador compartilhado a partir de muitas threads em um loop apertado?
Was this page helpful?