«O que significa synchronized?» — a pergunta que atormenta um em cada dois juniores
\n\nSe você já escreveu uma aplicação multithread em Java, certamente já se deparou com esta situação: duas threads alteram simultaneamente a mesma variável e, como resultado, bugs que ocorrem uma vez a cada cem execuções. Familiar? É exatamente para isso que existe a palavra-chave synchronized. Mas o que ela realmente faz? Como funciona "por baixo dos panos"? E, o mais importante, quando usá-la e quando é melhor evitá-la?
Neste artigo, não vamos apenas recontar a documentação. Vamos analisar uma pergunta popular do Stack Overflow (aquela que reuniu milhares de visualizações), adaptá-la para um formato educacional e mostrar uma comparação de abordagens: sincronização no nível do método, no nível do bloco e alternativas do java.util.concurrent. Vamos lá!
O problema: Condição de Corrida (Race Condition)
\n\nImagine: você tem um contador que é incrementado por duas threads. O código é simples, mas o resultado é inesperado.
\n\npublic class Counter {\n private int count = 0;\n\n public void increment() {\n count++;\n }\n\n public int getCount() {\n return count;\n }\n}\n\nIniciamos duas threads, cada uma faz 1000 incrementos. Aparentemente, deveria ser 2000. Mas na prática — 1987, 1993, ou até 1765. Por quê? Porque a operação count++ não é atômica. Ela consiste em três etapas: leitura → incremento → escrita. As threads se "sobrepõem" e uma alteração é perdida.
Isso é uma race condition — condição de corrida. E o synchronized é uma das principais ferramentas para combatê-la.
O que o synchronized faz? Uma olhada por baixo do capô
\n\nA palavra-chave synchronized em Java garante exclusão mútua (mutex) e visibilidade de alterações entre threads. Quando uma thread entra em um bloco synchronized, ela captura o monitor do objeto (ou classe). Outras threads que tentam entrar em qualquer bloco synchronized no mesmo monitor são bloqueadas até que ele seja liberado.
Importante: o synchronized funciona apenas com um monitor. Se você tiver dois objetos diferentes, a sincronização não funcionará.
Dois níveis de sincronização
\n- \n
- Método sincronizado — bloqueia o método inteiro, usa o monitor do objeto (
this). \n - Bloco sincronizado — bloqueia apenas parte do código, você pode especificar qualquer objeto como monitor. \n
Vamos compará-los na prática.
\n\nComparação de abordagens: método vs bloco
\n\n1. Método sincronizado
\n\nA maneira mais simples é adicionar synchronized à assinatura do método.
public class SafeCounter {\n private int count = 0;\n\n public synchronized void increment() {\n count++;\n }\n\n public synchronized int getCount() {\n return count;\n }\n}\n\nVantagens:
\n- \n
- Mínimo de código, fácil de ler. \n
- Garante que todo o método seja uma operação atômica. \n
Desvantagens:
\n- \n
- Bloqueio "pesado" — mesmo que dentro do método haja apenas uma linha, a thread é bloqueada durante toda a execução. \n
- Reduz o desempenho se o método for longo ou chamado com frequência. \n
- Não é flexível — não é possível sincronizar apenas parte da lógica. \n
2. Bloco sincronizado
\n\nUm controle mais refinado. Você decide qual objeto usar como monitor.
\n\npublic class SafeCounterWithBlock {\n private int count = 0;\n private final Object lock = new Object();\n\n public void increment() {\n synchronized (lock) {\n count++;\n }\n }\n\n public int getCount() {\n synchronized (lock) {\n return count;\n }\n }\n}