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:
- Tenta adquirir o monitor do objeto ao qual o
synchronizedestá associado. - Se o monitor não tiver proprietário, ele o adquire (agora
owner == self) e prossegue. - Se o monitor pertencer a outra thread, esta thread passa para o estado
BLOCKEDe entra na fila de espera. - 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:
- Não bloqueia os dados.
synchronized (list)não impede que outro código toque emlist; impede que outra thread segure o mesmo monitor. Se algum outro caminho de código operar emlistsem adquirir o mesmo monitor, a proteção desaparece. - 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. - Não acelera nada. Bloqueios são overhead puro. Use-os apenas onde a correção exige.
- 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 quecontainsKeyeputsejam individualmente thread-safe — o intervalo entre as duas chamadas não está protegido. UseputIfAbsentou 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 usarsynchronized (yourInstance); isso permite que um chamador segure seu bloqueio por quanto tempo quiser. Umprivate final Object lock = new Object();é seu e mais ninguém pode pegá-lo. - Não sincronize em literais
Stringou primitivos encaixotados. Eles são internados/cacheados; dois blocossynchronized ("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)ondemyFieldpode 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.
O que tirar da execução:
- A linha
unsafeperdeu atualizações de forma consistente — o valor final ficou abaixo do esperado1_000_000. Duas threads fazendon++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
synchronizedfaz; a parte menos visível é que o valor quevalue()lê é o mais recente escrito porincrement— 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 methodesync blockforam ambos visivelmente maiores queunsafe. 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 formasync methodbloqueia emthis, 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 dethis;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).