W3docs

Palavra-chave volatile em Java

Use volatile em Java para garantir visibilidade e ordenação de leituras e escritas de campos entre threads — sem usar bloqueio.

synchronized resolve dois problemas de uma vez: exclusão mútua e visibilidade de memória. Às vezes você só precisa do segundo. Um simples flag booleano que uma thread define e outra thread consulta não precisa de acesso exclusivo — há apenas um escritor e um ou vários leitores, e o trabalho em si não se compõe. Bloqueá-lo é exagero; não bloqueá-lo é problemático. volatile é a palavra-chave que fornece visibilidade sem exclusão.

É uma ferramenta específica. Acerte o caso de uso e será o primitivo thread-safe mais rápido que o Java possui. Erre — tente usá-lo para atualizações compostas como ++ — e você terá os mesmos bugs que synchronized deveria corrigir.

O problema que volatile existe para resolver

Sem nenhuma sincronização:

class Worker implements Runnable {
  boolean stop = false;                                 // plain field
  public void run() {
    while (!stop) {                                     // hot loop
      doWork();
    }
  }
}

Worker w = new Worker();
new Thread(w).start();
Thread.sleep(1000);
w.stop = true;                                          // ask it to stop

Este programa pode nunca parar. O motivo: nada na JVM é obrigado a liberar stop do cache de CPU de uma thread para a memória principal, e nada é obrigado a invalidar a cópia em cache do worker quando a thread principal escreve. O JIT também pode içar !stop para fora do loop completamente — o compilador vê que o corpo não modifica stop, então armazena o valor em um registrador. Para sempre.

Este é o bug de visibilidade. A correção:

volatile boolean stop = false;

Agora toda escrita em stop é liberada para a memória principal e toda leitura vem da memória principal. O JIT não pode içar a leitura; o loop vê o novo valor em microssegundos após a atualização do escritor.

O que volatile garante

Quatro coisas:

  1. Visibilidade. Uma escrita em um campo volatile é garantidamente visível para leituras subsequentes em qualquer outra thread.
  2. Atomicidade de leitura e escrita — mas apenas para o campo em si. Uma leitura/escrita de volatile long e volatile double é atômica; sem volatile, uma leitura/escrita de 64 bits pode ser fragmentada em uma JVM de 32 bits.
  3. Ordenação (happens-before). Tudo que aconteceu antes de uma escrita volatile na thread escritora é visível para qualquer coisa que aconteça depois da leitura volatile correspondente na thread leitora. Esta é a parte que você não vê na sintaxe, mas é a garantia mais poderosa.
  4. Sem reordenação em torno do acesso. O compilador e a CPU não podem mover leituras/escritas comuns além de um acesso volatile em nenhuma direção.

O terceiro — happens-before via um par de escrita/leitura volatile — é às vezes chamado de o primitivo de publicação mais barato. Se você escrever um campo e depois um volatile boolean ready = true, outra thread que vê ready == true tem garantia de também ver a escrita anterior. É assim que muita inicialização thread-safe é feita sem bloqueio.

O que volatile não garante

O erro mais comum:

volatile int counter = 0;

void increment() { counter++; }                       // STILL BROKEN

volatile torna a leitura atômica e a escrita atômica — mas counter++ é leitura-modificação-escrita, que são três operações. Duas threads ambas leem 42, ambas computam 43, ambas escrevem 43. Um incremento ainda é perdido.

Para atualizações compostas, volatile não é suficiente. Use synchronized, um Lock, ou — quase sempre a resposta correta — um AtomicInteger.

A outra coisa que volatile não faz:

  • Não fornece exclusão mútua. Duas threads podem escrever em um campo volatile simultaneamente; o resultado é "uma delas vence, a outra é perdida," que é a resposta certa para estado tipo flag e a resposta errada para "mesclar esses dois valores."
  • Não sincroniza múltiplos campos atomicamente juntos. Se seu invariante é "se A é verdadeiro então B deve ser 42," escrever cada um em um campo volatile separado pode deixar outra thread ver o novo A e o antigo B.

O padrão de publicação

A aplicação mais útil de volatile fora do caso de stop-flag:

class LazyResource {
  private Resource cached;                            // not volatile
  private volatile boolean ready = false;             // the publication flag

  public Resource get() {
    if (!ready) {
      synchronized (this) {
        if (!ready) {
          cached = buildResource();                   // expensive init
          ready = true;                               // publishes cached
        }
      }
    }
    return cached;
  }
}

A escrita volatile de ready = true publica a escrita anterior em cached. Qualquer thread que subsequentemente lê ready == true tem garantia de ver o cached totalmente inicializado. O bloqueio só é disputado na primeira chamada; chamadas subsequentes apenas leem ready e pulam a sincronização completamente. Este é o idioma clássico de double-checked locking, tornado correto pelo volatile.

Sem volatile em ready, a otimização está quebrada — a segunda thread pode ver ready == true e então ler cached como null. Com volatile, isso não pode acontecer.

(A alternativa moderna é simplesmente usar private final Resource cached = buildResource(); se a inicialização antecipada for adequada, ou Supplier<Resource> lazy = Suppliers.memoize(...) da sua biblioteca favorita. O double-checked locking manual é raro no código moderno; o padrão vale a pena conhecer porque você vai encontrá-lo.)

Quando volatile é exatamente certo

Um pequeno conjunto de casos de uso onde volatile é a melhor resposta:

  • Um flag de status de único escritor lido por muitas threads. Sinais de parada, flags "ready", números de versão de configuração.
  • Uma referência a um valor imutável que é trocado atomicamente. Cache que o escritor reconstrói e publica; os leitores veem o objeto antigo ou novo inteiro, nunca um meio construído.
  • Publicar o resultado de inicialização única por meio do padrão de double-checked-locking.
  • Um timestamp ou número de sequência que uma thread define e outras leem — onde perder a atualização mais recente é aceitável, mas o valor deve sempre ser autocoerente.

Fora esses casos, você geralmente quer uma ferramenta de nível mais alto. Não seja esperto com volatile — sua semântica é fina demais para suportar estado geral.

volatile long e volatile double em JVMs de 32 bits

Uma nuance histórica. Em uma JVM de 32 bits, uma leitura/escrita de long ou double não volatile tem permissão de ser dividida em dois acessos de 32 bits. Duas threads podem produzir um valor fragmentado — os 32 bits superiores de uma escrita e os 32 bits inferiores de outra. volatile long e volatile double têm garantia de serem atômicos independentemente do tamanho da palavra.

As JVMs modernas quase sempre rodam 64 bits, e JVMs de 64 bits tornam todos os primitivos atômicos de qualquer forma, mas a regra ainda está na especificação. Se você tem um long/double compartilhado entre threads, marque-o como volatile (ou use AtomicLong/AtomicDouble).

Um exemplo trabalhado: o bug do stop-flag

O programa abaixo executa o worker com um booleano simples primeiro (que geralmente nunca para), depois com um booleano volatile (que para prontamente). Um watchdog corta o caso quebrado após 2 segundos para que a demo termine.

java— editable, runs on the server

O que tirar da execução:

  • O worker PLAIN frequentemente não parou dentro da janela do watchdog. A thread principal escreveu stop = true; a thread worker, com sua cópia de stop armazenada em registrador, nunca percebeu. Adicione volatile e o bug desaparece. Este é o problema de visibilidade — todo programa multithread tem uma versão dele.
  • O worker VOLATILE parou em um ou dois microssegundos após a escrita. Esse é o custo da barreira de memória que a JVM emite para um par de escrita/leitura volatile — dezenas de microssegundos no pior caso, e frequentemente menor. Barato para o caso de uso correto.
  • O contador VOLATILE++ foi menor que 200.000. volatile não torna n++ em uma operação atômica — ainda é leitura-modificação-escrita, e duas threads podem ambas ler 42 e ambas armazenar 43. Este é o uso incorreto mais comum de volatile. Para atualizações compostas, use AtomicInteger (próximo capítulo).
  • O custo de volatile é pequeno mas real — toda leitura e toda escrita toca a memória principal e emite uma barreira de memória. Para um flag que é lido uma vez por iteração do loop, isso é nada. Para um campo que é lido em um loop interno intenso, o custo da barreira se acumula. Se você encontrar um volatile intenso em um perfil, considere se a leitura pode ser içada para uma variável local e o corpo do loop executado nesse snapshot.
  • volatile também é o mecanismo de publicação para uma referência — escreva os dados, depois faça a escrita volatile da referência. A thread leitora que vê a nova referência tem garantia de ver todos os dados que o escritor preparou. Esse é o bloco de construção dos padrões de double-checked-locking e troca de valor imutável.

O que vem a seguir

O próximo capítulo, Java Atomic Variables, apresenta AtomicInteger, AtomicLong, AtomicReference, e o restante da família java.util.concurrent.atomic — a ferramenta certa para "incrementar um contador de muitas threads" e qualquer outra atualização composta sem bloqueio.

Prática

Prática
Você declara `private volatile int counter = 0;` e chama `counter++` de múltiplas threads. Após 4 threads cada uma fazer 100.000 incrementos, qual valor `counter` tipicamente possui?
Você declara `private volatile int counter = 0;` e chama `counter++` de múltiplas threads. Após 4 threads cada uma fazer 100.000 incrementos, qual valor `counter` tipicamente possui?
Was this page helpful?