O que são variáveis voláteis (volatile) e quando usá-las?

Variáveis voláteis garantem visibilidade imediata das alterações feitas por uma thread para todas as outras threads.

O que são variáveis voláteis (volatile) e quando usá-las?

A palavra-chave volatile em Java é usada para garantir que mudanças feitas em uma variável por uma thread sejam imediatamente visíveis para todas as outras threads. Em aplicações concorrentes, o uso de volatile ajuda a evitar problemas de inconsistência de memória causados pelo cache das threads.

1. Problema sem volatile

Quando uma variável não é volatile, cada thread pode manter uma cópia em cache dela, levando a comportamentos inesperados:

class SemVolatile {
    private static boolean ativo = true;

    public static void main(String[] args) {
        new Thread(() -> {
            while (ativo) {
                // Loop infinito, pois a thread pode não ver a mudança de "ativo"
            }
            System.out.println("Thread encerrada");
        }).start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {}

        ativo = false; // A mudança pode não ser percebida pela thread
        System.out.println("Valor alterado para false");
    }
}

Explicação: A thread principal altera ativo = false, mas a thread secundária pode não perceber essa mudança devido ao cache de CPU.

2. Solução com volatile

O uso de volatile garante que todas as threads acessem sempre o valor mais recente da variável:

class ComVolatile {
    private static volatile boolean ativo = true;

    public static void main(String[] args) {
        new Thread(() -> {
            while (ativo) {
                // Agora a mudança será percebida corretamente
            }
            System.out.println("Thread encerrada");
        }).start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {}

        ativo = false;
        System.out.println("Valor alterado para false");
    }
}

Explicação: Agora, sempre que ativo for modificado, todas as threads verão o novo valor imediatamente.

3. Regras de Uso de volatile

  • Garante visibilidade, mas não atomicidade: volatile não impede condições de corrida em operações compostas (ex: contador++).
  • Ideal para flags de controle: Útil para variáveis de controle de loop, como ativo = false.
  • Não substitui synchronized ou Lock: Se houver mais de um passo na modificação da variável, use synchronized ou ReentrantLock.

4. Quando NÃO Usar volatile

Se a variável precisa de operações atômicas (exemplo: incremento x++), volatile não é suficiente. Para isso, use AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

public class ExemploAtomic {
    private static AtomicInteger contador = new AtomicInteger(0);

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                contador.incrementAndGet();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                contador.incrementAndGet();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {}

        System.out.println("Contador final: " + contador.get());
    }
}

Conclusão

A palavra-chave volatile melhora a visibilidade das variáveis entre threads, mas não impede condições de corrida. Seu uso é indicado para variáveis imutáveis ou flags de controle, mas para operações atômicas, recomenda-se synchronized, Lock ou AtomicInteger.

A sincronização de variáveis em aplicações multithread é um dos desafios mais complexos da programação concorrente. O uso incorreto de cache de CPU pode levar a comportamentos inesperados, onde uma thread não enxerga as mudanças feitas por outra. A palavra-chave volatile é uma solução simples, mas limitada, pois apenas garante a visibilidade das alterações e não evita condições de corrida. Para cenários onde múltiplas operações precisam ser sincronizadas, o uso de synchronized, Locks ou classes atômicas como AtomicInteger é mais adequado.

Algumas aplicações:

  • Garantia de visibilidade de mudanças de estado em aplicações concorrentes
  • Implementação de variáveis de controle (ex: flags de interrupção)
  • Evitar problemas de cache em sistemas multithread
  • Melhoria da comunicação entre threads sem a necessidade de sincronização completa

Dicas para quem está começando

  • Use volatile apenas para variáveis de controle de estado
  • Evite usar volatile em variáveis que requerem operações atômicas
  • Se precisar de sincronização completa, utilize synchronized ou Locks
  • Para contadores e incrementos seguros, prefira AtomicInteger

Contribuições de Rodrigo Farias

Compartilhe este tutorial: O que são variáveis voláteis e quando usá-las

Compartilhe este tutorial

Continue aprendendo:

Como criar um pool de threads em Java

Um pool de threads em Java gerencia múltiplas tarefas concorrentes, otimizando o uso dos recursos do sistema.

Tutorial anterior

Como evitar Race Conditions em Java

Race Conditions ocorrem quando múltiplas threads acessam e modificam a mesma variável simultaneamente, causando resultados imprevisíveis.

Próximo tutorial