Variáveis Volatile em Java
O modificador volatile fornece um mecanismo de sincronização que garante a visibilidade das alterações de uma variável entre threads, sem a necessidade de bloqueios explícitos.
Durante a execução de um programa, as threads podem armazenar cópias locais de variáveis em cache, e tanto o compilador quanto o processador podem reordenar instruções para otimização. Isso pode causar inconsistências quando uma thread atualiza uma variável e outra continua lendo um valor antigo.
Ao declarar uma variável como volatile, a JVM é instruída a não armazená-la em cache por thread. Assim, qualquer modificação feita em uma thread torna-se imediatamente visível às demais.
Problema de visibilidade entre threads
De acordo com o Java Memory Model (JMM), cada thread mantém uma memória de trabalho (work memory), geralmente implementada como cache do processador. Quando uma variável é modificada, essa alteração pode ficar apenas no cache da thread, e as demais threads podem continuar lendo o valor antigo da memória principal.
O volatile resolve exatamente esse problema: ele força as leituras e escritas a acontecerem diretamente na memória principal.
Exemplo sem volatile
public class Program {
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker();
Thread workerThread = new Thread(worker);
workerThread.start(); // Inicia o "Thread A"
Thread.sleep(1000); // Espera 1 segundo
worker.stop(); // "Thread B" envia o comando para parar
workerThread.join(2000); // Aguarda até 2 segundos
if (workerThread.isAlive()) {
System.out.println("--- RESULTADO: Thread A ainda está ativa! ---");
System.out.println("Thread A não viu a mudança de running = false.");
System.exit(1);
} else {
System.out.println("--- RESULTADO: Thread A terminou com sucesso. ---");
}
}
}
class Worker implements Runnable {
// Flag SEM volatile
private boolean running = true;
@Override
public void run() {
System.out.println("Thread A: iniciando trabalho...");
while (running) {
// Loop contínuo
}
System.out.println("Thread A: trabalho encerrado.");
}
public void stop() {
System.out.println("Thread B: enviando sinal de parada...");
this.running = false;
}
}Saída possível:
Thread A: iniciando trabalho... Thread B: enviando sinal de parada... --- RESULTADO: Thread A ainda está ativa! --- Thread A não viu a mudança de running = false.
O problema ocorre porque o Thread A provavelmente manteve o valor de running em cache e nunca consultou novamente a memória principal.
Corrigindo com volatile
Basta alterar a declaração da variável:
private volatile boolean running = true;Com volatile, a thread é obrigada a ler o valor diretamente da memória principal a cada iteração do loop, garantindo que a alteração feita pela outra thread seja percebida imediatamente.
O que o volatile garante
O volatile oferece duas garantias principais:
1. Garantia de Visibilidade (Visibility)
- Escritas em uma variável
volatilesão imediatamente refletidas na memória principal. - Leituras em uma variável
volatilesempre acessam a memória principal, ignorando qualquer valor em cache local.
Isso garante que todas as threads enxerguem o mesmo valor atualizado.
2. Garantia de Ordenação (Happens-Before)
O volatile também impede certos tipos de reordenação de instruções, tanto pelo compilador quanto pelo processador.
Ele estabelece a seguinte relação:
- A escrita em uma variável
volatileacontece depois de todas as operações anteriores na mesma thread. - A leitura de uma variável
volatileacontece antes de todas as operações subsequentes.
Em outras palavras, o volatile atua como uma barreira de memória (memory barrier).
Volatile não garante atomicidade
O volatile resolve o problema de visibilidade, mas não o de atomicidade.
Veja o exemplo a seguir:
private volatile int counter = 0;
public void increment() {
counter++; // NÃO É SEGURO!
}A operação counter++ parece simples, mas é composta por três etapas:
- Leitura do valor atual (
counter). - Incremento do valor.
- Escrita do novo valor.
Se duas threads executarem increment() ao mesmo tempo, ambas podem ler o mesmo valor antes que qualquer uma o atualize, resultando em perda de incremento.
Exemplo de possível interleaving:
- Thread A lê
counter = 0. - Thread B lê
counter = 0. - Thread A grava
1. - Thread B grava
1.
O valor final é 1, quando deveria ser 2.
Para garantir atomicidade, é necessário usar mecanismos como synchronized, Lock, ou classes atômicas como AtomicInteger.
Quando usar volatile
O volatile deve ser usado apenas quando:
- As operações não dependem do valor atual da variável.
(Exemplo: definir
flag = true, e nãoflag = !flag). - A variável é escrita por apenas uma thread, mas pode ser lida por várias outras.
Casos típicos incluem:
- Flags de controle (
running,active,shutdown). - Sinalização de eventos entre threads.
- Estado de configuração leve que não exige sincronização complexa.
Resumo
volatilegarante visibilidade e ordenação, mas não atomicidade.- Força leituras e escritas diretas na memória principal.
- Deve ser usado apenas para variáveis compartilhadas que não participam de operações compostas.
- É ideal para flags de controle entre threads.
- Para operações dependentes do valor atual (como contadores), utilize
AtomicIntegerou blocossynchronized.