Usamos cookies essenciais para login e armazenamento local para suas preferências. Para métricas de uso, usamos o Google Analytics 4 — você pode recusar sem perder nada do produto. Saber mais

ThreadMineThreadMine
Voltar

Guia técnico

Como identificar e resolver deadlocks em Java?

Deadlock é dois códigos certos que, quando rodam juntos, paralisam a aplicação. Guia direto para encontrar, ler e impedir esses casos.

Testar grátis

Deadlock é um daqueles bugs que só aparece em produção. Dois trechos de código, cada um funcionando perfeitamente em testes unitários, quando executam ao mesmo tempo entram num impasse circular: a thread A segura o lock 1 e quer o 2; a thread B segura o lock 2 e quer o 1. Ninguém solta, ninguém progride, e o restante do sistema vai degradando até que alguém reinicia. Foi assim no incidente clássico do banco em 2012, no payment gateway que comeu uma sexta à noite em 2019, e provavelmente foi assim na última vez que você viu um endpoint não responder mais sem motivo aparente.

A boa notícia: a JVM já sabe identificar deadlock sozinha. A melhor notícia: você pode detectar isso em testes automatizados antes do código subir. Este artigo mostra como.

O que é um deadlock

Em termos formais, deadlock é um conjunto de processos onde cada um espera por um recurso detido por outro processo do mesmo conjunto. Em Java, os "recursos" são tipicamente monitores de objeto (synchronized) ou instâncias de ReentrantLock, mas também podem ser semáforos, conexões de pool ou até threads ociosas de um ExecutorService.

Diferente de uma thread lenta, no deadlock nenhuma das threads envolvidas progride. A única intervenção possível sem reiniciar a JVM é matar uma das threads — o que em geral significa corromper estado e tomar dump rápido antes que algo pior aconteça.

As quatro condições de Coffman

Edward Coffman provou em 1971 que um deadlock requer simultaneamente quatro condições. Quebrar qualquer uma delas elimina a possibilidade do impasse.

  1. Exclusão mútua. O recurso só pode ser detido por uma thread por vez. (Quase todo lock em Java tem isso.)
  2. Posse e espera. Uma thread detém um recurso e aguarda por outro. (Acontece quando você abre um lock dentro de outro lock.)
  3. Sem preempção. O sistema não tira o recurso da thread à força. (JVM não preempta monitores.)
  4. Espera circular. Existe um ciclo de threads onde cada uma espera o recurso da seguinte. (Esta é a condição mais fácil de quebrar.)

Em código Java, a estratégia padrão é atacar a quarta: ordenação global de locks (sempre que precisar mais de um lock, adquira-os em ordem fixa). Vamos ver isso mais adiante.

Como detectar (código e dump)

Pela API: ThreadMXBean

A JVM expõe um detector em tempo de execução. Em testes de integração, você pode verificar deadlock automaticamente:

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
    public static long[] detectar() {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        long[] ids = bean.findDeadlockedThreads();
        return ids == null ? new long[0] : ids;
    }
}

Em produto sério, vale rodar isso a cada 60 segundos como guarda-corpo e exportar métrica Micrometer; se em algum momento o array não for vazio, dispare alerta P1 imediatamente.

Pelo dump

A JVM HotSpot escreve a seção Found one Java-level deadlock ao final do thread dump quando detecta o impasse — você não precisa reconstruir nada, a própria JVM já fez a análise topológica dos locks.

Exemplo: deadlock clássico em duas threads

O exemplo mais didático — e ainda assim o que mais causa incidente real — é o de duas threads adquirindo dois locks em ordens opostas:

public class Transferencia {
    private final Object contaA = new Object();
    private final Object contaB = new Object();

    public void aParaB() {
        synchronized (contaA) {
            // ... trabalho com A
            synchronized (contaB) {
                // ... atualiza saldo
            }
        }
    }

    public void bParaA() {
        synchronized (contaB) {
            // ... trabalho com B
            synchronized (contaA) {     // <-- ordem invertida
                // ... atualiza saldo
            }
        }
    }
}

Quando aParaB e bParaA rodam concorrentemente, na maior parte das vezes uma termina antes da outra começar e nada acontece. Em momentos de carga, eventualmente as duas vão executar simultaneamente: a primeira pega contaA, a segunda pega contaB, e as duas ficam paradas esperando o lock que a outra detém. Os testes unitários continuam verdes. O bug só manifesta em produção, sob carga, intermitentemente — o pior tipo.

Lendo a seção Found one Java-level deadlock

Em um dump tirado nesse cenário, você encontra ao final algo como:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fb44c004ce8 (object 0x00000007a3c41e80, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fb44c004d88 (object 0x00000007a3c41e90, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
  at com.exemplo.Transferencia.bParaA(Transferencia.java:17)
  - waiting to lock <0x00000007a3c41e80> (a java.lang.Object)
  - locked <0x00000007a3c41e90> (a java.lang.Object)
"Thread-0":
  at com.exemplo.Transferencia.aParaB(Transferencia.java:8)
  - waiting to lock <0x00000007a3c41e90> (a java.lang.Object)
  - locked <0x00000007a3c41e80> (a java.lang.Object)

Found 1 deadlock.

A leitura é linear. O bloco "Found one Java-level deadlock" é a confirmação de que a JVM, não você, identificou o problema — então não há falso positivo aqui. Os endereços hexadecimais são identificadores únicos dos objetos: 0x...e80 e 0x...e90 são as duas instâncias detidas em ordem trocada. A pilha mostra exatamente em que linha cada thread bateu. Em 30 segundos você tem o diagnóstico completo.

Como evitar deadlocks na prática

  1. Lock ordering global. Estabeleça uma ordem total entre locks (ex: pelo identificador do objeto). Toda thread que precisa de dois locks os adquire na mesma ordem. Quebra a condição de espera circular.
  2. tryLock com timeout. Em vez de synchronized aninhado, use ReentrantLock.tryLock(timeout, unit) e recue se não conseguir adquirir em N segundos. Quebra a condição de "sem preempção".
  3. Evite sincronização aninhada. Se você está segurando um lock dentro de outro lock, pare e procure refatorar. Mover trabalho para fora do bloco interno é a mudança mais simples e a que mais elimina deadlock.
  4. Prefira estruturas concorrentes. ConcurrentHashMap, CopyOnWriteArrayList, AtomicReference eliminam a necessidade de synchronized em muitos casos.
  5. Mantenha sincronização no mínimo possível. Trabalho dentro de bloco sincronizado deveria ser quase atômico — ler/atualizar um campo, não chamar IO nem serviço externo.
  6. Teste pessimista. Em testes de integração, rode ThreadMXBean.findDeadlockedThreads() após cada cenário concorrente.

Falsos deadlocks e livelocks

Nem toda paralisia é deadlock. Três parentes próximos a distinguir:

  • Livelock — threads continuam executando, mas em círculos, sem progresso útil (ex: dois algoritmos de retry que sempre colidem).
  • Starvation — uma thread nunca pega o lock porque outras sempre o pegam antes (problema de fairness).
  • Pool exhaustion travado — todas as threads do pool ocupadas esperando uma resposta externa. Se você reinicia o serviço externo, libera; logo, não é deadlock real.

A diferença prática: se findDeadlockedThreads() retorna vazio mas o sistema está parado, você tem uma das três situações acima — não um deadlock clássico.

Como o ThreadMine detecta automaticamente

Ao receber um dump, o ThreadMine faz duas verificações ortogonais. Primeiro lê a seção Found one Java-level deadlock emitida pela própria JVM — se ela existe, mostra direto o ciclo, as threads envolvidas e os endereços dos monitores com classificação CRITICAL. Segundo, reconstrói o grafo de dependência de locks a partir das pilhas (quem segura X e quem espera X) e procura ciclos por conta própria — útil para dumps de versões antigas da JVM ou de implementações que não escrevem essa seção automaticamente.

Cada thread envolvida no ciclo vira um card com severidade e a pilha relevante, e o grafo de locks aparece na visualização de dependências (com detecção de ciclos). Leve um dump e o ThreadMine te diz em segundos se o que você está vendo é deadlock ou outra coisa parecida.

Conclusão

Deadlock é prevenível com poucas regras: nunca segure dois locks na mesma rotina sem ordem global, prefira tryLock com timeout, mantenha o bloco sincronizado o menor possível. Quando aparecer, a JVM resolve a parte chata da análise — você só precisa saber ler. E quando a fila de incidentes crescer, automatize a leitura.

Quer ver tudo isso aplicado ao seu dump?

Faça upload e tenha health score, problemas detectados e sugestões em segundos. Sem cadastro para o primeiro dump.