Entendendo o Java Memory Model

O Java Memory Model (JMM) é um componente fundamental da linguagem Java, que define como as variáveis são acessadas e modificadas em um ambiente multithread. Compreender o JMM é crucial para escrever programas concorrentes corretos e eficientes. Neste post, vamos explorar os conceitos principais do JMM em detalhes, com exemplos práticos.
O Que é o Java Memory Model?
O JMM especifica as regras que a JVM (Java Virtual Machine) segue para ler e escrever variáveis na memória. Ele garante que as operações sejam visíveis e atômicas, e controla a reordenação de instruções pelo compilador e pelo processador.
Visibilidade de Memória
A visibilidade de memória refere-se à garantia de que as alterações feitas por uma thread em uma variável sejam visíveis para outras threads. Sem essas garantias, uma thread pode não ver as atualizações feitas por outra, levando a comportamentos inesperados.
Exemplo de Problema de Visibilidade
Vamos ver um exemplo:
public class ExemploVisibilidade {
private static boolean pronto = false;
private static int valor = 0;
public static void main(String[] args) throws InterruptedException {
Thread escritor = new Thread(() -> {
valor = 42;
pronto = true;
});
Thread leitor = new Thread(() -> {
while (!pronto) {
// Espera ocupada
}
System.out.println("Valor: " + valor);
});
escritor.start();
leitor.start();
escritor.join();
leitor.join();
}
}
No exemplo acima, a thread leitor
pode nunca sair do loop, pois a alteração na variável pronto
feita pela thread escritor
pode não ser visível para ela. Isso ocorre porque as threads podem ter seus próprios caches de variáveis, e a mudança feita por uma thread pode não ser imediatamente visível para outra.
Para resolver esse problema, usamos a palavra-chave volatile
:
private static volatile boolean pronto = false;
Quando uma variável é declarada como volatile
, o JMM garante que todas as leituras e escritas nessa variável sejam feitas diretamente na memória principal, em vez de em caches de thread, garantindo visibilidade entre threads.
Para mais detalhes sobre a palavra-chave volatile
, consulte este post aqui no blog: Programação concorrente em Java: Explicação sobre a palavra reservada Volatile.
Atomicidade
A atomicidade garante que as operações sejam executadas completamente ou não sejam executadas. Em Java, operações simples de leitura e escrita em variáveis do tipo int
, long
e boolean
são atômicas, mas operações compostas, como incrementos, não são.
Exemplo de Problema de Atomicidade
Considere o seguinte exemplo:
public class ExemploAtomicidade {
private static int contador = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
contador++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
contador++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Contador: " + contador);
}
}
No exemplo acima, o valor do contador
pode não ser 2000 devido a condições de corrida. O incremento contador++
não é uma operação atômica; ele consiste em ler o valor atual de contador
, incrementá-lo e escrever o novo valor de volta, e essas operações podem ser intercaladas entre as threads.
Para garantir a atomicidade, usamos AtomicInteger
:
import java.util.concurrent.atomic.AtomicInteger;
public class ExemploAtomicidade {
private static AtomicInteger contador = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
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();
t1.join();
t2.join();
System.out.println("Contador: " + contador);
}
}
O método incrementAndGet()
é atômico, garantindo que o valor do contador
será corretamente atualizado mesmo quando acessado por múltiplas threads.
Reordenação de Instruções
A reordenação de instruções é uma técnica usada pelo compilador e pelo processador para otimizar a execução do código. No entanto, essa reordenação pode causar problemas em ambientes multithread se não for controlada corretamente.
Exemplo de Problema de Reordenação
Considere o seguinte exemplo:
public class ExemploReordenacao {
private static int a = 0, b = 0;
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("(" + x + ", " + y + ")");
}
}
No exemplo acima, o resultado (0, 0)
é possível devido à reordenação de instruções. As atribuições a = 1
e x = b
podem ser reordenadas pelo compilador ou pelo processador, assim como b = 1
e y = a
, levando a resultados inconsistentes.
Para evitar problemas de reordenação, podemos usar blocos synchronized
ou outras construções de sincronização:
public class ExemploReordenacaoCorrigido {
private static int a = 0, b = 0;
private static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (ExemploReordenacaoCorrigido.class) {
a = 1;
x = b;
}
});
Thread t2 = new Thread(() -> {
synchronized (ExemploReordenacaoCorrigido.class) {
b = 1;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("(" + x + ", " + y + ")");
}
}
O uso de synchronized
garante que o código dentro do bloco seja executado de maneira atômica e visível para outras threads, evitando reordenações indesejadas.
Conclusão
Entender o Java Memory Model é essencial para escrever programas concorrentes corretos e eficientes. A visibilidade de memória, a atomicidade e a reordenação de instruções são conceitos cruciais que você deve dominar para evitar erros sutis e difíceis de depurar. Utilizando palavras-chave como volatile
, classes como AtomicInteger
e construções de sincronização, você pode garantir que suas threads funcionem de forma correta e previsível. Com prática e estudo contínuo, você se tornará mais confiante no desenvolvimento de aplicativos concorrentes em Java.