1. Aprofundando em deadlock
Deadlock (“bloqueio mútuo”) — é a situação em que duas ou mais threads esperam umas pelas outras indefinidamente: cada uma mantém algum recurso e tenta obter outro que já está sendo mantido por uma thread vizinha. No fim, ninguém consegue continuar e o programa “trava”. É como dois carros em uma ponte estreita frente a frente: enquanto um não der ré — ninguém passa.
Em Java, deadlock não é uma exceção (Exception), e sim um “congelamento” do programa. As threads não finalizam nem caem com erro, apenas esperam umas pelas outras para sempre. Por isso esses erros são traiçoeiros: eles só se manifestam “sob certas circunstâncias”.
Quatro condições para que o deadlock ocorra
- 1. Exclusão mútua (Mutual Exclusion)
Um recurso pode ser adquirido por apenas uma thread por vez (por exemplo, um objeto-monitor, um arquivo). - 2. Retenção e espera (Hold and Wait)
A thread mantém um recurso e tenta obter um segundo, sem liberar o primeiro. - 3. Sem preempção (No Preemption)
O recurso não pode ser “tomado” da thread: apenas a própria thread pode liberá-lo. - 4. Espera circular (Circular Wait)
Há uma cadeia fechada em que cada thread espera por um recurso mantido pela próxima na cadeia.
Um deadlock só é possível se TODAS essas condições forem satisfeitas simultaneamente. Basta quebrar ao menos uma — e o bloqueio mútuo se torna impossível.
2. Exemplo de código com deadlock
Esquema
- Há dois recursos: lock1 e lock2 (objetos comuns).
- A Thread A primeiro adquire lock1, depois tenta obter lock2.
- A Thread B primeiro adquire lock2, depois tenta obter lock1.
Exemplo (não faça isso em casa!)
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Thread 1
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: adquiriu lock1");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 1: tentando adquirir lock2");
synchronized (lock2) {
System.out.println("Thread 1: adquiriu lock2");
}
}
});
// Thread 2
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: adquiriu lock2");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 2: tentando adquirir lock1");
synchronized (lock1) {
System.out.println("Thread 2: adquiriu lock1");
}
}
});
thread1.start();
thread2.start();
}
}
O que acontece ao executar?
- A Thread 1 adquire lock1, a Thread 2 — lock2.
- Ambas “dormem” por 100 milissegundos, conseguindo adquirir o primeiro lock.
- Em seguida, cada uma tenta adquirir o segundo lock, que já está sendo mantido pela outra thread.
- Ambas as threads esperam uma pela outra indefinidamente — ocorreu um deadlock.
No console você verá:
Thread 1: adquiriu lock1
Thread 2: adquiriu lock2
Thread 1: tentando adquirir lock2
Thread 2: tentando adquirir lock1
Por que o deadlock ocorre? Análise detalhada
- Exclusão mútua: cada lock pode ser mantido por apenas uma thread.
- Retenção e espera: a Thread 1 mantém lock1 e espera por lock2; a Thread 2 mantém lock2 e espera por lock1.
- Sem preempção: ninguém pode “arrancar” o lock de fora — apenas saindo do bloco synchronized.
- Espera circular: a espera se fechou em ciclo entre as duas threads.
3. Como evitar e prevenir deadlock
Adquira recursos sempre na mesma ordem
Regra principal: se várias threads precisam adquirir vários recursos, faça isso sempre na mesma ordem. Por exemplo: primeiro lock1, depois lock2 — em todos os pontos do código.
public void doSomething() {
Object firstLock = lock1;
Object secondLock = lock2; // ordem única "lock1 -> lock2"
synchronized (firstLock) {
synchronized (secondLock) {
// Trabalho com ambos os recursos
}
}
}
Com uma ordem única, a espera circular se torna impossível — a condição de deadlock é quebrada.
Uso de tryLock com timeout (ReentrantLock)
Quando não se sabe previamente quais recursos serão necessários, use ReentrantLock e o método tryLock para não esperar indefinidamente.
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public void doWork() {
try {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Seção crítica
} finally {
lock2.unlock();
}
} else {
System.out.println("Não foi possível adquirir lock2, desfazendo");
}
} finally {
lock1.unlock();
}
} else {
System.out.println("Não foi possível adquirir lock1, desfazendo");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Vantagem: é possível desfazer a operação e tentar novamente depois. Desvantagem: o código fica mais complexo, mas o deadlock é evitado.
Minimize o tempo de retenção dos locks
Mantenha o lock pelo menor tempo possível. Faça dentro de synchronized/lock somente o necessário; mova todo o resto para fora da seção crítica.
Evite locks aninhados
Quanto menos synchronized/lock aninhados, menor o risco de bloqueio mútuo. Se possível — use um único lock para recursos relacionados.
Use estruturas thread-safe prontas
A biblioteca padrão já resolveu muitos problemas: coleções como ConcurrentHashMap e outras classes de java.util.concurrent minimizam o risco de deadlock e simplificam o código.
4. Diagnose de deadlock
Thread Dump e jstack
Thread Dump — um instantâneo do estado de todas as threads na JVM.
Obtendo via console:
jstack <pid>
Onde <pid> — o identificador do processo Java (pode ser obtido via jps).
Nas IDEs geralmente há um botão “Thread Dump” no painel de depuração.
No dump, procure por:
- Status BLOCKED ou WAITING.
- Mensagens do tipo waiting to lock ... e locked ....
- A frase da JVM: "Found one Java-level deadlock:" — quando um bloqueio mútuo é detectado.
Exemplo de trecho do dump:
"Thread-1":
waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
which is held by "Thread-1"
Isto é um deadlock: as threads “seguram” uma à outra.
VisualVM e outras ferramentas
- VisualVM — ferramenta gratuita para análise de threads e detecção de deadlock.
- Java Mission Control / Flight Recorder — monitoramento e profiling avançados.
No VisualVM é possível ver a árvore de threads, seus estados e receber um aviso sobre bloqueio mútuo.
Tabela: “Como não cair em deadlock”
| Causa do deadlock | Como evitar |
|---|---|
| Aquisição de recursos em ordens diferentes | Sempre manter uma única ordem de aquisição de recursos |
| synchronized aninhados | Minimizar o aninhamento; se possível, usar um único lock |
| Manter o lock por muito tempo | Reduzir o trabalho dentro da seção crítica |
| Uso simultâneo de vários locks | Aplicar tryLock com timeout e ser capaz de desfazer |
| Coleções não thread-safe | Usar coleções concorrentes (ConcurrentHashMap etc.) |
5. Erros comuns ao lidar com deadlock
Erro nº 1: aquisição de recursos em ordens diferentes. A causa mais frequente — threads diferentes adquirem locks em ordens diferentes. Mesmo que “fique mais rápido”, mantenha uma ordem única — caso contrário, a espera circular é garantida.
Erro nº 2: synchronized aninhados sem necessidade. Aninhamento extra aumenta o risco de bloqueio mútuo e complica o código. Simplifique o modelo de locks.
Erro nº 3: ignorar tryLock e timeouts. O bloqueio via synchronized fará a thread esperar indefinidamente. Se não houver certeza, use tryLock com timeout e lógica de desfazer.
Erro nº 4: executar código pesado dentro do lock. Chamadas de rede, I/O e computações pesadas sob lock aumentam muito a probabilidade de problemas. Mova-os para fora da seção crítica.
Erro nº 5: não analisar o Thread Dump. Thread Dump é seu melhor aliado na busca por bloqueios mútuos. Use jstack e analise os estados BLOCKED/WAITING, cadeias “holding/waiting to lock”.
GO TO FULL VERSION