CodeGym /Cursos /JAVA 25 SELF /Java Memory Model (JMM)

Java Memory Model (JMM)

JAVA 25 SELF
Nível 58 , Lição 4
Disponível

1. Introdução ao Java Memory Model (JMM)

O problema de visibilidade e ordenação

Em um programa de thread única tudo é simples: você grava um valor em uma variável — e pode lê-lo imediatamente. Na realidade multithreaded isso funciona de outra forma. Os processadores fazem cache de valores, o compilador e a JVM às vezes trocam a ordem das instruções, e uma thread pode ver um valor “antigo”, mesmo que outra thread tenha acabado de alterá-lo.

Java Memory Model (JMM) descreve como as threads se comunicam pela memória: quando as alterações de uma thread se tornam visíveis para as outras e em que ordem as operações ocorrem. Se isso não for considerado, o programa pode se comportar de forma imprevisível, embora à primeira vista tudo pareça normal.

Entender o JMM ajuda a saber por que às vezes a thread não vê dados recentes, como usar corretamente volatile, synchronized e classes atômicas, e por que bugs em código concorrente podem aparecer só em produção. Em poucas palavras, o JMM são as regras do jogo da memória e, se você as ignorar, até o código mais cuidadoso pode jogar contra você.

Analogia

Imagine que você tem duas pessoas (threads) que escrevem e leem bilhetes (variáveis) em um quadro (memória). Às vezes uma escreve e a outra ainda não vê a nova anotação — porque está olhando para sua própria cópia do quadro (cache). O JMM define quando e como esses bilhetes se tornam visíveis para todos.

2. happens-before: o fundamento do JMM

O que é happens-before?

happens-before é a relação entre duas ações no programa: se a ação A acontece-before a ação B, então todas as alterações feitas em A são garantidamente visíveis em B.

Importante: happens-before não é simplesmente “aconteceu antes”, mas sim “é garantidamente visível”.

Regras principais de happens-before

1. Dentro de uma única thread

Tudo o que acontece em uma thread é ordenado: se você gravou em uma variável e depois a leu — verá sua própria alteração.

2. Blocos/monitores sincronizados

Tudo o que ocorre antes da saída de um bloco sincronizado (synchronized) se torna visível para a thread que depois entrar nesse bloco.

synchronized(lock) {
    sharedVar = 42; // escrita
}
// ...
synchronized(lock) {
    System.out.println(sharedVar); // veremos 42 garantidamente
}

3. escrita/leitura de volatile

Uma escrita em um campo volatile acontece-before qualquer leitura subsequente desse campo por outra thread.

volatile boolean ready = false;

// Thread 1
data = 123;
ready = true; // volatile write

// Thread 2
if (ready) { // volatile read
    System.out.println(data); // garantido: veremos data = 123
}

4. Início e término de threads

  • A chamada de Thread.start() acontece-before o início da execução da thread.
  • O término da thread acontece-before o retorno de Thread.join().

5. Conclusão de tarefa no Executor

Se você enviou uma tarefa para um Executor e aguardou sua conclusão (Future.get()), todas as alterações feitas na tarefa são visíveis após o get().

6. Campos finais

A inicialização de campos com o modificador final no construtor acontece-before a publicação da referência ao objeto. Isso é importante para objetos imutáveis.

3. Publicação segura de objetos

Problema: “objeto com valores desatualizados”

Se uma thread cria um objeto e o entrega para outra thread sem sincronização, a outra thread pode ver valores “crus” nos campos (por exemplo, não inicializados ou antigos).

class Holder {
    int value;
    Holder() { value = 42; }
}

Holder holder = null;

// Thread 1
holder = new Holder(); // criamos o objeto

// Thread 2
if (holder != null) {
    System.out.println(holder.value); // pode ver 0, não 42!
}

Como publicar objetos corretamente?

1. Por meio de campos final

Se todos os campos do objeto são final e são inicializados no construtor, o objeto pode ser publicado com segurança sem sincronização adicional.

class SafeHolder {
    final int value;
    SafeHolder() { value = 42; }
}

2. Por meio de uma referência volatile

Se a referência ao objeto é declarada como volatile, após a atribuição o objeto fica garantidamente visível para outras threads.

volatile Holder holder;

// Thread 1
holder = new Holder();

// Thread 2
if (holder != null) {
    System.out.println(holder.value); // veremos 42 garantidamente
}

3. Por meio de inicialização em thread única antes da publicação

Se o objeto é criado e inicializado antes que a referência a ele se torne acessível a outras threads, tudo é seguro.

Holder holder = new Holder(); // apenas em uma thread
// ... depois holder se torna acessível a outras threads

4. Por meio de bloqueio

Se o objeto é criado dentro de um bloco sincronizado e a referência a ele é lida somente dentro de um bloco igual, tudo é seguro.

Holder holder;

synchronized(lock) {
    if (holder == null) {
        holder = new Holder();
    }
}

// ... em outra thread
synchronized(lock) {
    if (holder != null) {
        // seguro
    }
}

4. Double-checked locking e volatile

O que é double-checked locking?

É um padrão para inicialização preguiçosa de um objeto singleton:

class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) { // primeira verificação
            synchronized (Singleton.class) {
                if (instance == null) { // segunda verificação
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Problema: Sem uma referência volatile para instance, esse código não funciona corretamente! Uma thread pode ver um objeto não totalmente inicializado.

Por que sem volatile é ruim?

A JVM pode “reordenar” as instruções de modo que a referência ao objeto seja atribuída antes da conclusão do construtor. Outra thread verá um objeto não inicializado.

Como fazer corretamente?

Declare instance como volatile:

private static volatile Singleton instance;

Agora o double-checked locking funciona corretamente: volatile garante a relação happens-before entre a escrita e a leitura da referência.

Alternativa: inicialização estática

O jeito mais simples e seguro de fazer um singleton é usar inicialização estática:

class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    public static Singleton getInstance() { return INSTANCE; }
}

Aqui a JVM garante a inicialização correta.

5. VarHandle: acesso moderno de baixo nível

O que é VarHandle?

VarHandle é uma API moderna (Java 9+) que permite trabalhar com variáveis em baixo nível: ler, escrever, fazer operações atômicas, controlar visibilidade e ordenação de instruções.

Para que serve o VarHandle se existem classes atômicas?
VarHandle permite trabalhar com quaisquer campos (não apenas com int/long/Reference).
— Permite escolher explicitamente a semântica de acesso: volatile, acquire/release, opaque.
— É usado para implementar estruturas de dados de alto desempenho.

Semânticas de acesso

  • Volatile: garantia completa de happens-before (como em um campo volatile).
  • Acquire/Release: garantia mais fraca, porém mais rápida (usada para estruturas lock-free).
  • Opaque: garantia mínima de visibilidade, mas máxima performance.

Exemplo de uso de VarHandle

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

class Counter {
    int value;
    static final VarHandle VALUE_HANDLE;

    static {
        try {
            VALUE_HANDLE = MethodHandles.lookup().findVarHandle(Counter.class, "value", int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

Counter counter = new Counter();
Counter.VALUE_HANDLE.setVolatile(counter, 42);
int v = (int) Counter.VALUE_HANDLE.getVolatile(counter);

Quando usar VarHandle?

  • Para implementar suas próprias estruturas de dados lock-free.
  • Quando é necessária máxima performance e controle sobre a ordem das instruções.
  • Em aplicações comuns, geralmente as classes atômicas e synchronized são suficientes.

6. False sharing e alinhamento de cache

False sharing é a situação em que duas threads trabalham com variáveis diferentes, mas essas variáveis estão na mesma linha de cache do processador. Como resultado, as threads atrapalham uma à outra, porque a alteração de uma variável invalida o cache da outra.

Analogia: Duas pessoas sentam-se na mesma mesa (linha de cache), mas cada uma escreve em sua metade. Se uma muda algo, a outra precisa “reler” toda a folha.

Por que isso é ruim?

  • A performance cai drasticamente: os processadores gastam tempo sincronizando caches.
  • É especialmente crítico para variáveis “quentes”, que são frequentemente alteradas por diferentes threads.

Como evitar?

Separe os campos “quentes” em objetos diferentes ou use anotações/estruturas especiais para alinhamento (por exemplo, @Contended). Em JVMs modernas é possível ativar a opção -XX:-RestrictContended e usar @sun.misc.Contended (Java 8+) para alinhar campos.

Exemplo:

@sun.misc.Contended
public volatile long value1;

@sun.misc.Contended
public volatile long value2;

NB: A anotação @Contended não faz parte da API padrão, mas é usada no JDK para otimização de classes atômicas.

7. Prática: corrigindo o singleton e microbenchmarks JMH

Corrigindo o singleton defeituoso

Ruim (sem volatile):

class BrokenSingleton {
    private static BrokenSingleton instance;
    public static BrokenSingleton getInstance() {
        if (instance == null) {
            synchronized (BrokenSingleton.class) {
                if (instance == null) {
                    instance = new BrokenSingleton();
                }
            }
        }
        return instance;
    }
}

Bom (com volatile):

class SafeSingleton {
    private static volatile SafeSingleton instance;
    public static SafeSingleton getInstance() {
        if (instance == null) {
            synchronized (SafeSingleton.class) {
                if (instance == null) {
                    instance = new SafeSingleton();
                }
            }
        }
        return instance;
    }
}

Melhor de tudo — inicialização estática:

class StaticSingleton {
    private static final StaticSingleton INSTANCE = new StaticSingleton();
    public static StaticSingleton getInstance() { return INSTANCE; }
}

Microbenchmarks JMH: visibilidade e atomicidade

Atenção: JMH é um framework específico para microbenchmarks em Java. Não tente tirar conclusões de desempenho sem o JMH — os resultados podem enganar!

Exemplo: verificando visibilidade de volatile

public class VolatileVisibility {
    volatile boolean flag = false;

    public void writer() {
        flag = true;
    }

    public void reader() {
        while (!flag) {
            // loop até ver true
        }
        // mudança observada
    }
}

Exemplo: a não atomicidade de volatile

public class VolatileNotAtomic {
    volatile int counter = 0;

    public void increment() {
        counter++; // não é atômico!
    }
}

Apesar de volatile, ao incrementar simultaneamente a partir de várias threads o valor final será menor que o esperado (a operação counter++ se decompõe em leitura, cálculo e escrita).

8. Erros típicos ao trabalhar com JMM, volatile e publicação

Erro nº 1: esperar atomicidade de volatile.
volatile garante apenas visibilidade das alterações, não a atomicidade das operações. A operação counter++ não se torna atômica se a variável for volatile.

Erro nº 2: publicar objetos sem sincronização.
Se você cria um objeto em uma thread e o entrega a outra sem volatile, synchronized ou campos final, a outra thread pode ver valores “crus”.

Erro nº 3: double-checked locking sem volatile.
Sem uma referência volatile no singleton, você pode obter um objeto não inicializado em outra thread.

Erro nº 4: uso de bloqueios antigos com threads virtuais.
Alguns mecanismos antigos de sincronização (native monitor) podem atrapalhar a JVM a gerenciar eficientemente threads virtuais.

Erro nº 5: ignorar false sharing.
Se variáveis “quentes” estão próximas na memória, as threads vão se atrapalhar por causa das linhas de cache.

Erro nº 6: tirar conclusões de performance sem JMH.
Microbenchmarks sem JMH frequentemente produzem resultados enganosos devido às otimizações da JVM e aos caches do processador.

1
Pesquisa/teste
Aprofundando em multithreading, nível 58, lição 4
Indisponível
Aprofundando em multithreading
Aprofundando em multithreading
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION