Atualizado: 02/11/2025

Este conteúdo é original e não foi gerado por inteligência artificial.

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 volatile são imediatamente refletidas na memória principal.
  • Leituras em uma variável volatile sempre 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 volatile acontece depois de todas as operações anteriores na mesma thread.
  • A leitura de uma variável volatile acontece 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:

  1. Leitura do valor atual (counter).
  2. Incremento do valor.
  3. 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:

  1. As operações não dependem do valor atual da variável. (Exemplo: definir flag = true, e não flag = !flag).
  2. 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

  • volatile garante 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 AtomicInteger ou blocos synchronized.
Política de Privacidade

Copyright © www.programicio.com Todos os direitos reservados

É proibida a reprodução do conteúdo desta página sem autorização prévia do autor.

Contato: programicio@gmail.com