1. Unlock/release esquecido: armadilha para desatentos
Um dos erros mais traiçoeiros ao usar ferramentas modernas de sincronização, como ReentrantLock ou Semaphore, é esquecer de chamar unlock() ou release(). Se você não liberar o bloqueio, outras threads vão esperar pela liberação dele... para sempre. O programa vai travar e você vai ficar olhando para a tela, tentando entender por que nada acontece.
Vamos ver um exemplo com ReentrantLock:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
// Opa! Esquecemos de chamar unlock() — agora todos vão travar!
count++;
}
}
Parece inofensivo, mas se você chamar increment() várias vezes de threads diferentes, após a primeira chamada as demais threads vão esperar a liberação do bloqueio indefinidamente.
Para evitar isso, use o padrão try-finally:
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
Agora, mesmo que uma exceção ocorra no meio do método, o bloqueio será liberado de forma garantida.
É como se alguém ocupasse o banheiro (trancando por dentro) e depois esquecesse de destrancar a porta e saísse pela janela. Os outros vão ficar esperando até essa pessoa “sair”... Não faça isso!
2. Sincronizar no objeto errado: “Ops, pendurei o cadeado no lugar errado!”
Em Java, a palavra-chave synchronized pode bloquear o acesso a um objeto. Mas se você escolher o objeto errado para bloquear, a sincronização não vai funcionar como você espera.
Erro nº 1: sincronizar em uma variável local
public void doSomething() {
Object lock = new Object();
synchronized (lock) {
// Toda vez é um objeto novo — não há sincronização!
// As threads não esperam umas pelas outras.
// A seção crítica não está protegida!
}
}
Aqui, cada thread cria seu próprio objeto lock. Como resultado, nenhum bloqueio real acontece — as threads entram na seção crítica ao mesmo tempo.
Correto:
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// Agora todas as threads usam o mesmo objeto lock
// e de fato esperam umas pelas outras.
}
}
Erro nº 2: sincronizar em um literal de string
public void doSomething() {
synchronized ("lock") {
// Literais de string são internados: partes diferentes do programa podem
// acidentalmente sincronizar no mesmo literal!
}
}
Conclusão:
Sincronize apenas em objetos privados, criados especificamente para isso, que não sejam usados em nenhum outro lugar.
3. Bloqueio mútuo (deadlock): “Você espera por mim, eu por você — e ninguém anda”
Deadlock (bloqueio mútuo) é um clássico. Duas (ou mais) threads adquirem, alternadamente, bloqueios diferentes e ficam esperando uma pela outra, até o programa travar completamente.
Exemplo:
public class DeadlockExample {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) {
// Vamos esperar um pouco para fins de demonstração
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockB) {
// ...
}
}
}
public void method2() {
synchronized (lockB) {
try { Thread.sleep(50); } catch (InterruptedException e) {}
synchronized (lockA) {
// ...
}
}
}
}
Se uma thread chamar method1() e outra — method2(), a primeira thread vai adquirir lockA e esperar por lockB, enquanto a segunda fará o oposto. Como resultado, ambas vão esperar indefinidamente.
Como evitar?
- Sempre adquira os locks na mesma ordem em todas as threads.
- Minimize a quantidade de locks mantidos simultaneamente.
- Use ferramentas de diagnóstico (por exemplo, jstack) se o programa travar.
Analogia:
É como se duas pessoas se encontrassem em um corredor estreito, e cada uma decidisse ceder a passagem, mas somente se a outra cedesse primeiro. No fim, ambas ficam paradas esperando até que alguém desista.
4. Sincronização excessiva: “Melhor pecar pelo excesso?” — nem sempre!
Às vezes, com medo de erros, desenvolvedores sincronizam tudo. Como resultado, a performance cai e o benefício é zero.
Exemplo:
public synchronized void add(int value) {
// Aqui há apenas uma linha que não requer sincronização!
System.out.println("Adicionado: " + value);
}
Neste caso, a sincronização não é necessária: a saída na tela via System.out.println já é thread-safe, e o próprio método não trabalha com recursos compartilhados.
Onde isso é crítico?
Se você sincroniza métodos que são chamados com frequência e não precisam de proteção, reduz drasticamente o desempenho do programa. As threads formam uma fila, embora pudessem trabalhar em paralelo.
Best practice:
Sincronize apenas o que realmente for necessário. A seção crítica deve ser a menor possível.
5. Uso incorreto de volatile: “Há visibilidade, mas não há atomicidade!”
O modificador volatile em Java garante que as mudanças na variável serão visíveis para todas as threads. Mas ele não garante a atomicidade das operações.
Erro:
private volatile int counter = 0;
public void increment() {
counter++; // Não é atômico!
}
A operação counter++ consiste em ler o valor, incrementá-lo e gravá-lo de volta. Se duas threads executarem esse código ao mesmo tempo, o valor final pode ser menor do que o esperado.
Correto:
Para operações atômicas, use synchronized, AtomicInteger ou outras classes thread-safe.
import java.util.concurrent.atomic.AtomicInteger;
private final AtomicInteger counter = new AtomicInteger();
public void increment() {
counter.incrementAndGet();
}
Quando usar volatile?
Para flags simples (por exemplo, “encerrar execução”), quando não é necessária atomicidade.
GO TO FULL VERSION