DEV Community

João Antônio
João Antônio

Posted on

JVM, Java Memory Model e CPU: por que funciona em x86 e quebra em ARM

“Mas nunca deu problema na minha máquina.”

Provavelmente porque sua máquina é x86. Troca pra ARM e alguns bugs de concorrência que estavam “dormindo” aparecem.

A ideia central: x86 costuma ser mais “conservador” na prática, enquanto ARM permite mais reordenações. Se seu código depende de comportamento “bonzinho” do hardware, ele pode sobreviver em x86 e falhar em ARM.


O que o Java Memory Model (JMM) realmente garante

O JMM não fala “threads compartilham memória e pronto”. Ele define:

  • visibilidade: quando o que uma thread escreveu passa a ser visto por outra
  • ordem: quais reordenações são permitidas
  • happens-before: a relação que cria garantias reais entre threads

Sem um happens-before entre duas ações, você não tem garantia de:

  • ver o valor mais recente
  • ver as coisas na ordem que você escreveu
  • ver um objeto “pronto”

Isso é o motivo de muitos bugs “fantasma”.


O erro comum: confiar na ordem do código

Você escreve:

x = 42;
ready = true;
Enter fullscreen mode Exit fullscreen mode

E imagina que outra thread, ao ver ready == true, vai necessariamente ver x == 42.

O JMM permite que, sem sincronização, outra thread observe:

ready == true
x == 0

Enter fullscreen mode Exit fullscreen mode

Porque:

  • x pode ficar em cache/registrador
  • as escritas podem ser reordenadas
  • a outra thread pode ler valores antigos

O clássico pesado: “objeto meio construído”

Esse é o bug mais traiçoeiro.

instance = new MyObject();

Isso parece uma operação, mas por baixo vira algo tipo:

  1. alocar memória

  2. inicializar campos (construtor)

  3. publicar a referência (instance aponta pro objeto)

Sem sincronização, a JVM/CPU podem efetivamente permitir que a publicação (3) aconteça antes da inicialização (2) ser visível para outras threads.

Então uma segunda thread pode ver:

if (instance != null) {
    instance.doSomething();
}
Enter fullscreen mode Exit fullscreen mode

E instance não é null, mas o objeto pode estar com campos ainda em estado “default” (0/null). Em padrões como double-checked locking isso já deu dor real em produção.

Onde volatile entra (e por que resolve)

Se você declara:

private static volatile MyObject instance;
Enter fullscreen mode Exit fullscreen mode

Você ganha duas coisas importantes:

  • Visibilidade: leitura de volatile vê o valor mais recente (não “preso” em cache local).
  • Ordem: volatile cria barreiras que impedem certas reordenações ao redor da variável.

Regra que importa:

Uma escrita em um volatile acontece-before de qualquer leitura posterior do mesmo volatile.

Na prática: se a thread B leu instance (volatile) como não-nulo, ela passa a ter garantia de enxergar as escritas que aconteceram antes da thread A publicar essa referência (incluindo os writes do construtor que “prepararam” o objeto).

“Como funciona por baixo dos panos”

Escrita em volatile

Quando a JVM compila uma escrita em volatile, ela precisa garantir que:

todas as escritas anteriores não fiquem “penduradas” e invisíveis

a publicação do valor não “passe na frente” de coisas anteriores

Isso é implementado inserindo memory fences/barriers ao redor do acesso (o tipo exato depende da arquitetura e do JIT).

Leitura de volatile

Quando a JVM compila uma leitura de volatile, ela precisa garantir que:

  • você não leia um valor velho do cache/registrador
  • leituras seguintes não sejam movidas “pra antes” dessa leitura

Também usa barreiras e instruções com semântica apropriada.

x86 vs ARM: por que a diferença aparece

x86 (modelo mais forte na prática)

Em x86, muitas reordenações são menos agressivas e a plataforma tende a “ajudar” sem você pedir. Não significa que o código está correto — significa que o bug pode não se manifestar.

Resultado: muita gente escreve código sem happens-before e “passa”.

ARM (modelo mais fraco)

ARM permite mais reordenação e exige sincronização explícita para garantir ordem/visibilidade. Se você não usou volatile/synchronized/locks/atomics, ARM tem mais chance de mostrar o bug.

Resultado: o mesmo programa “ok” em x86 pode falhar em ARM sob carga.

Regra prática (sem filosofia)

Se tem compartilhamento entre threads, escolha uma estratégia:

  • volatile: bom para flags/estado simples e publicação segura de referência
  • synchronized/Lock: bom para invariantes e operações compostas

  • Atomic*: bom para operações atômicas sem lock (CAS), com custos/limites próprios

Se você não tem happens-before, você está apostando na arquitetura e no acaso.

Top comments (0)