W3docs

Sincronização em Java

Coordene o acesso ao estado compartilhado entre threads em Java com a palavra-chave synchronized e os bloqueios intrínsecos.

A introdução ao multithreading alertou sobre três modos de falha — corridas, bugs de visibilidade e deadlocks. synchronized é a primeira resposta do Java às duas primeiras. Ele oferece a um bloco de código duas garantias ao mesmo tempo: exclusão mútua (apenas uma thread por vez dentro do bloco) e visibilidade de memória (escritas feitas dentro do bloco por uma thread são vistas pela próxima thread que entrar nele). Essas duas garantias combinadas são suficientes para tornar uma grande quantidade de código multithreaded correto.

Este capítulo é o conceitual — o que synchronized faz, o que é um monitor intrínseco, que tipos de corridas ele resolve e quais não resolve. O próximo capítulo, blocos synchronized, apresenta as formas sintáticas e como escolher entre elas.

A corrida que a palavra-chave existe para corrigir

class Counter {
  int n;
  void increment() { n++; }
}

Counter c = new Counter();
// Thread A and Thread B both call c.increment() a million times.
// After both finish, what is c.n?

n++ é uma linha de código-fonte e três operações de bytecode: carregar n, somar 1, armazenar n. Se a thread A carrega n=42, depois a thread B carrega n=42 antes de A armazenar, ambas somam 1 e ambas armazenam 43. Um incremento é perdido. Execute o programa um milhão de vezes em cada thread e c.n será consistentemente menor que 2_000_000.

synchronized é a solução:

class Counter {
  int n;
  synchronized void increment() { n++; }
}

Agora apenas uma thread por vez executa increment neste Counter. A outra espera na porta. Resultado: c.n == 2_000_000, em todas as execuções.

O que é um monitor

Todo objeto Java possui, escondido dentro da JVM, um bloqueio associado chamado monitor intrínseco (ou monitor lock). É apenas uma estrutura de dados do tipo long com dois pedaços de estado: uma thread proprietária (ou null) e uma fila de espera. Uma thread que entra em um bloco synchronized:

  1. Tenta adquirir o monitor do objeto ao qual o synchronized está associado.
  2. Se o monitor não tiver proprietário, ele o adquire (agora owner == self) e prossegue.
  3. Se o monitor pertencer a outra thread, esta thread passa para o estado BLOCKED e entra na fila de espera.
  4. Quando o proprietário sai do bloco, a JVM libera o monitor e um dos que estavam esperando o obtém.

O monitor é por objeto. Duas instâncias de Counter têm dois monitores separados; threads operando em Counters diferentes não se bloqueiam. Isso é importante — a sincronização está no objeto, não "no método".

synchronized (someObject) {
  // critical section: only one thread at a time
  // holds someObject's monitor inside this block
}

synchronized em um método de instância é um atalho para synchronized (this). Em um método estático, é um atalho para synchronized (Counter.class) — o monitor do objeto Class.

Visibilidade, não apenas exclusão

A exclusão mútua é a parte óbvia. A parte menos óbvia — e mais importante — é o relacionamento happens-before que a JVM oferece gratuitamente:

Tudo que uma thread faz antes de liberar um monitor é garantido que será visível para qualquer thread que posteriormente adquirir o mesmo monitor.

Essa frase é o que torna synchronized correto, não apenas "primeiro a chegar, primeiro a ser atendido". Sem ela, duas threads podem usar um bloco synchronized, concordar com a exclusão mútua e ainda ver as escritas uma da outra em ordem errada — porque os caches da CPU e o JIT, caso contrário, são livres para reordenar. O par release/acquire instala uma barreira de memória que força a CPU e o JIT a descarregar e recarregar.

A implicação: qualquer campo que um programa multithreaded leia ou escreva fora de um bloco synchronized (e não via volatile, um atômico ou outro primitivo de java.util.concurrent) não tem garantia de visibilidade. Uma thread pode escrever done = true e outra thread pode ver done = false para sempre. Voltaremos a isso quando abordarmos volatile e o modelo de memória do Java.

O que synchronized não corrige

Quatro coisas que iniciantes frequentemente esperam do synchronized e que ele não entrega:

  1. Não bloqueia os dados. synchronized (list) não impede que outro código toque em list; impede que outra thread segure o mesmo monitor. Se algum outro caminho de código operar em list sem adquirir o mesmo monitor, a proteção desaparece.
  2. Não compõe entre objetos. synchronized (a); synchronized (b); são duas aquisições separadas; se outra thread as adquirir na ordem inversa, você terá um deadlock.
  3. Não acelera nada. Bloqueios são overhead puro. Use-os apenas onde a correção exige.
  4. Não corrige todas as corridas. Ações compostas como "verificar e agir" ainda competem mesmo com cada operação individual sincronizada. if (map.containsKey(k)) map.put(k, v) está incorreto mesmo que containsKey e put sejam individualmente thread-safe — o intervalo entre as duas chamadas não está protegido. Use putIfAbsent ou um único bloco sincronizado ao redor de ambos.

Reentrância

O monitor intrínseco é reentrante: uma thread que já detém um monitor pode entrar em outro bloco synchronized no mesmo objeto sem se bloquear. É por isso que isso funciona:

class Account {
  synchronized void deposit(int x) { balance += x; }
  synchronized void transferTo(Account other, int x) {
    deposit(-x);                                     // re-enters same monitor — fine
    other.deposit(x);                                // acquires other's monitor too
  }
}

Se os monitores não fossem reentrantes, a chamada interna a deposit bloquearia no monitor que a chamada externa já detém — deadlock instantâneo. A reentrância torna seguro chamar outro método sincronizado no mesmo objeto.

O lado oposto: cada aquisição precisa de uma liberação correspondente. A JVM mantém uma contagem; o monitor é liberado quando a contagem chega a zero.

Em que sincronizar

Algumas regras que evitam a maioria dos bugs de uso incorreto de bloqueios:

  • Sincronize em um objeto de bloqueio privado, não em this. Código externo também pode usar synchronized (yourInstance); isso permite que um chamador segure seu bloqueio por quanto tempo quiser. Um private final Object lock = new Object(); é seu e mais ninguém pode pegá-lo.
  • Não sincronize em literais String ou primitivos encaixotados. Eles são internados/cacheados; dois blocos synchronized ("foo") em partes diferentes do seu código compartilham um monitor com qualquer outro que também disse "foo".
  • Não sincronize em uma referência que pode mudar. synchronized (myField) onde myField pode ser reatribuído representa dois monitores diferentes ao longo do tempo. O compilador não consegue detectar; o bug é silencioso.
  • Mantenha a seção crítica pequena. Quanto mais você fizer dentro de um bloco synchronized, mais tempo todos os outros esperam. Segure o bloqueio enquanto altera o estado compartilhado, não enquanto faz o I/O ao redor.

Um exemplo prático: com e sem o bloqueio

O programa abaixo executa a mesma carga de trabalho de contador compartilhado de três formas: sem sincronização, método synchronized e bloco synchronized em um objeto de bloqueio dedicado. Os números mostram que a primeira forma perde atualizações e as outras duas não.

java— editable, runs on the server

O que tirar da execução:

  • A linha unsafe perdeu atualizações de forma consistente — o valor final ficou abaixo do esperado 1_000_000. Duas threads fazendo n++ competem em read-modify-write; alguns incrementos desaparecem. Mesmo quando o teste passa em uma única execução por sorte, o JIT, o agendador do SO ou uma CPU diferente eventualmente vão revelar o problema. A mutação não sincronizada de um campo compartilhado está incorreta.
  • As duas variantes seguras produziram exatamente a contagem esperada, todas as vezes. A exclusão mútua é a parte óbvia do que synchronized faz; a parte menos visível é que o valor que value() lê é o mais recente escrito por increment — essa é a garantia de visibilidade. Sem o par de monitores, a leitura poderia legitimamente ver uma cópia obsoleta em cache.
  • Os números de tempo real para sync method e sync block foram ambos visivelmente maiores que unsafe. Bloqueios não são gratuitos — cada entrada/saída realiza uma barreira de memória e (sob contenção) uma troca de contexto de thread. Sincronize onde a correção exige; não espalhe por "segurança".
  • A variante sync block on private lock é o que o código de produção usa. A forma sync method bloqueia em this, que qualquer chamador externo também pode adquirir — eles podem te privar de recursos segurando seu próprio bloqueio. Um objeto de bloqueio privado que você nunca expõe é somente seu.
  • O bloco de reentrância executou sem deadlock. outer() já segurava o monitor de this; inner() o re-entrou sem bloquear. É por isso que um método sincronizado pode livremente chamar outro método sincronizado no mesmo objeto — sem reentrância, metade da biblioteca padrão entraria em deadlock.

O que vem a seguir

O próximo capítulo, Blocos Synchronized em Java, aprofunda as formas sintáticas — método, bloco, estático — e as regras para escolher o objeto de bloqueio correto. Para coordenação de nível mais alto entre threads, veja comunicação entre threads (wait/notify).

Prática

Prática
Duas threads cada uma chama `counter.increment()` em um `Counter` cujo campo `n` é um `int` não sincronizado. Após ambas terminarem 1.000.000 de incrementos, o que `counter.n` tipicamente mostra?
Duas threads cada uma chama `counter.increment()` em um `Counter` cujo campo `n` é um `int` não sincronizado. Após ambas terminarem 1.000.000 de incrementos, o que `counter.n` tipicamente mostra?
Was this page helpful?