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 stopEste 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:
- Visibilidade. Uma escrita em um campo
volatileé garantidamente visível para leituras subsequentes em qualquer outra thread. - Atomicidade de leitura e escrita — mas apenas para o campo em si. Uma leitura/escrita de
volatile longevolatile doubleé atômica; semvolatile, uma leitura/escrita de 64 bits pode ser fragmentada em uma JVM de 32 bits. - Ordenação (happens-before). Tudo que aconteceu antes de uma escrita
volatilena thread escritora é visível para qualquer coisa que aconteça depois da leituravolatilecorrespondente na thread leitora. Esta é a parte que você não vê na sintaxe, mas é a garantia mais poderosa. - Sem reordenação em torno do acesso. O compilador e a CPU não podem mover leituras/escritas comuns além de um acesso
volatileem 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 BROKENvolatile 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
volatilesimultaneamente; 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
volatileseparado 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.
O que tirar da execução:
- O worker
PLAINfrequentemente não parou dentro da janela do watchdog. A thread principal escreveustop = true; a thread worker, com sua cópia destoparmazenada em registrador, nunca percebeu. Adicionevolatilee o bug desaparece. Este é o problema de visibilidade — todo programa multithread tem uma versão dele. - O worker
VOLATILEparou 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/leituravolatile— dezenas de microssegundos no pior caso, e frequentemente menor. Barato para o caso de uso correto. - O contador
VOLATILE++foi menor que200.000.volatilenão tornan++em uma operação atômica — ainda é leitura-modificação-escrita, e duas threads podem ambas ler42e ambas armazenar43. Este é o uso incorreto mais comum devolatile. Para atualizações compostas, useAtomicInteger(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 umvolatileintenso em um perfil, considere se a leitura pode ser içada para uma variável local e o corpo do loop executado nesse snapshot. volatiletambém é o mecanismo de publicação para uma referência — escreva os dados, depois faça a escritavolatileda 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.