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

Published: (December 14, 2025 at 09:17 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Introdução

“Mas nunca deu problema na minha máquina.”
Provavelmente porque sua máquina é x86. Troque para 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 o seu código depende de comportamento “bonzinho” do hardware, ele pode sobreviver em x86 e falhar em ARM.

Java Memory Model (JMM)

O JMM não diz apenas “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, não há garantia de:

  • ver o valor mais recente;
  • ver as coisas na ordem que foram escritas;
  • ver um objeto “pronto”.

Esse é o motivo de muitos bugs “fantasma”.

Exemplo clássico

x = 42;
ready = true;

Muitos imaginam que, ao ver ready == true, outra thread necessariamente verá x == 42. O JMM permite que, sem sincronização, a outra thread observe:

ready == true
x == 0

Por quê?

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

Esse é o bug mais traiçoeiro.

Publicação segura de objetos

instance = new MyObject();

Embora pareça uma única operação, por baixo acontece algo como:

  1. alocar memória;
  2. inicializar campos (construtor);
  3. publicar a referência (instance aponta para o 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. Assim, uma segunda thread pode executar:

if (instance != null) {
    instance.doSomething();
}

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

Uso de volatile

private static volatile MyObject instance;

Declarar a referência como volatile traz duas garantias importantes:

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

Uma escrita em um volatile happens‑before de qualquer leitura posterior do mesmo volatile. Na prática, se a thread B leu instance (volatile) como não‑nulo, ela tem garantia de enxergar todas as escritas que aconteceram antes da thread A publicar a referência (incluindo os writes do construtor que “prepararam” o objeto).

Como o JIT trata volatile

Escrita em volatile

Ao compilar uma escrita em volatile, a JVM deve garantir que:

  • todas as escritas anteriores não fiquem “penduradas” e invisíveis;
  • a publicação do valor não “passe na frente” de operações anteriores.

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

Leitura em volatile

Ao compilar uma leitura em volatile, a JVM deve garantir que:

  • você não leia um valor velho do cache/registrador;
  • leituras subsequentes não sejam movidas “para antes” dessa leitura.

Também são usadas barreiras e instruções com semântica apropriada.

x86 vs. ARM

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

Em ARM, há mais reordenações permitidas e a arquitetura exige sincronização explícita para garantir ordem e visibilidade. Se você não usou volatile, synchronized, locks ou atomics, ARM tem maior chance de expor o bug.

Consequência: o mesmo programa “ok” em x86 pode falhar em ARM sob carga.

Regra prática (sem filosofia)

Se há compartilhamento entre threads, escolha uma estratégia adequada:

EstratégiaQuando usar
volatileFlags/estado simples e publicação segura de referência
synchronized / LockInvariantes e operações compostas
Atomic*Operações atômicas sem lock (CAS), com custos/limites próprios

Se você não tem um happens‑before explícito, está apostando na arquitetura e no acaso.

Back to Blog

Related posts

Read more »

History of Java

Origins Java was started in 1991 by James Gosling as part of a research initiative called the Green Project. The main objective of this project was to create a...