1. Palavra‑chave synchronized: por que e como
Em Java, a palavra‑chave synchronized — é como uma placa “Ocupado!” na porta do banheiro: enquanto uma thread está dentro da “seção crítica”, as demais esperam educadamente a sua vez. Só quando a primeira sair, a próxima poderá entrar e executar seu código.
Sintaxe: bloco e método
Bloco sincronizado
synchronized (object) {
// seção crítica
}
- object — é qualquer objeto no qual você quer “pendurar o cadeado”. Enquanto uma thread executa esse bloco, outras threads que também querem entrar em um bloco com esse mesmo objeto vão esperar.
Método sincronizado
public synchronized void increment() {
// seção crítica
}
- Aqui, o “cadeado” é colocado no próprio objeto (this). Ou seja, apenas uma thread por vez pode executar qualquer método sincronizado desse objeto.
Método estático sincronizado
public static synchronized void foo() {
// seção crítica
}
- Aqui o bloqueio ocorre no nível da classe (ClassName.class), e não de um objeto específico.
Como isso funciona por baixo dos panos
Quando uma thread entra em um bloco ou método sincronizado, ela captura o “monitor” do objeto (ou da classe, para métodos estáticos). Se o monitor já estiver ocupado — a thread espera. Assim que o monitor é liberado, a próxima thread pode entrar.
2. Exemplo: incremento do contador com e sem sincronização
Sem sincronização
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class CounterDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Valor final: " + counter.getCount());
}
}
Valor esperado: 2000
Valor real: pode ser menor (por exemplo, 1995, 1987...), e a cada execução — uma “surpresa” diferente.
Por quê? Porque a operação count++ não é atômica: ela se divide em três passos — ler o valor, incrementar e gravar de volta. Se duas threads fizerem isso simultaneamente, elas podem sobrescrever o resultado uma da outra.
Solução: synchronized
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Agora apenas uma thread por vez pode executar o método increment(). O valor final sempre será 2000.
Alternativa: bloco sincronizado
public class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
}
O resultado será o mesmo. Você pode sincronizar não o método inteiro, mas apenas a parte necessária.
3. Introdução ao “monitor do objeto”
O monitor é um “cadeado” embutido em cada objeto em Java. Quando você escreve synchronized(object), a thread tenta “trancar” esse objeto. Se o cadeado estiver livre — a thread o obtém; caso contrário — ela espera sua vez. Assim que a thread sai do bloco, o cadeado é liberado.
Importante! Se você sincroniza em objetos diferentes — as threads não vão esperar umas pelas outras. Portanto, é muito importante escolher o objeto correto para sincronização.
Métodos estáticos sincronizados
Às vezes, o recurso compartilhado não é um objeto, mas algo comum a todas as instâncias da classe (por exemplo, uma variável estática). Nesse caso, a sincronização deve ser no nível da classe.
public class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
Isto é equivalente a:
public static void increment() {
synchronized (StaticCounter.class) {
count++;
}
}
O monitor fica no objeto da classe (Class), e não em uma instância específica.
4. Palavra‑chave volatile: o que é e para que serve
Problema de visibilidade entre threads
Em Java, cada thread pode armazenar em cache os valores das variáveis para acelerar a execução. Isso significa que, se uma thread alterou uma variável, outra thread pode “não perceber”, continuando a ler o valor do seu cache local. Isso é especialmente crítico para flags com as quais as threads sinalizam umas às outras.
Como funciona o volatile
Se uma variável for declarada como volatile, isso significa:
- Todas as threads sempre leem e escrevem nela apenas na memória principal, ignorando o cache.
- Qualquer alteração na variável se torna imediatamente visível para todas as threads.
Mas! As operações com volatile por si só não são atômicas (exceto leitura/gravação simples de primitivos como boolean, int etc.). Se você faz algo mais complexo do que uma atribuição — é necessária sincronização.
Exemplo: flag de encerramento
public class Worker extends Thread {
private volatile boolean running = true;
public void run() {
while (running) {
// fazemos algo útil
}
System.out.println("Thread finalizada");
}
public void shutdown() {
running = false;
}
}
Worker w = new Worker();
w.start();
// ... depois de algum tempo
w.shutdown();
Sem volatile, a thread pode “não perceber” a alteração do flag e entrar em loop para sempre (especialmente em sistemas multi-core). Com volatile — tudo funciona como esperado.
5. Limitações do volatile: não-atomicidade
Muitos iniciantes pensam: “Se eu tornar int volatile, posso escrever count++ e não me preocupar”. Infelizmente, não é bem assim:
private volatile int count = 0;
public void increment() {
count++;
}
Erro! A operação count++ ainda não é atômica — são três etapas: (1) ler, (2) incrementar, (3) gravar de volta. Se duas threads lerem o mesmo valor ao mesmo tempo, ambas o incrementarão e ambas gravarão o mesmo resultado — um incremento “se perde”.
Conclusão: volatile garante apenas a visibilidade das alterações, mas não protege contra condições de corrida em operações complexas.
6. Quando usar synchronized e quando — volatile
- volatile — quando você tem um flag simples (por exemplo, boolean), que uma thread escreve e outra lê. Exemplo: encerramento de uma thread, sinalização de um evento.
- synchronized — quando é preciso garantir a atomicidade de operações complexas (por exemplo, incremento, alteração de várias variáveis, trabalho com estruturas de dados).
Tabela para memorização
| Cenário | volatile | synchronized |
|---|---|---|
| Enviar sinal entre threads | ✔ | ✔ |
| Operação atômica (incremento) | ✖ | ✔ |
| Vários passos na seção crítica | ✖ | ✔ |
| Apenas visibilidade das alterações | ✔ | ✔ |
7. Erros comuns ao usar synchronized e volatile
Erro nº 1: Sincronizar no objeto errado. Se você sincroniza em uma variável local ou em objetos diferentes em cada thread — não haverá proteção alguma.
Object lock = new Object();
synchronized (lock) {
// ...
}
Se cada thread criar seu próprio lock — não adianta. É necessário um ponto único de sincronização, um objeto comum para todas as threads.
Erro nº 2: Esperar atomicidade de volatile. volatile garante visibilidade, não atomicidade. Operações como count++ continuam inseguras sem sincronização.
Erro nº 3: Sincronizar uma área muito grande do código. Se você sincroniza o método inteiro, quando é necessário — apenas uma linha, você bloqueia outras threads à toa e perde desempenho. Procure reduzir a “seção crítica”.
Erro nº 4: Esquecer de tornar a sincronização “estática” para dados estáticos. Se você tem uma variável estática, mas sincroniza em this, isso não ajuda. Para dados estáticos, é necessária sincronização no nível da classe: synchronized(ClassName.class).
Erro nº 5: Sincronizar em literal de string. Sincronizar em strings é perigoso, porque literais iguais são internados pela JVM. Você pode, sem querer, obter um bloqueio compartilhado para partes diferentes do programa.
GO TO FULL VERSION