1. Thread Dump e análise do estado das threads
Thread Dump (dump de threads) — é um instantâneo do estado de todas as threads do aplicativo em um determinado momento. É como uma foto em grupo de todas as suas threads: quem está fazendo o quê, quem está preso onde, quem está esperando quem. Thread Dump — seu principal instrumento para encontrar deadlock, livelock e outros travamentos misteriosos.
Como obter um Thread Dump?
Pelo terminal (jstack):
Se você tem o PID do processo Java, execute:
jstack <PID>
O comando imprimirá no console o estado de todas as threads, indicando em que estado cada uma está e quais monitores (locks) elas mantêm.
Pela IDE (IntelliJ IDEA):
No menu “Run” → “Show Running List” → selecione o processo → “Thread Dump”.
Pelo VisualVM ou JConsole:
Abra o processo, encontre a aba “Threads” e faça um snapshot do estado.
Exemplo de Thread Dump
Trecho do dump:
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e0c7800 nid=0x1a48 waiting for monitor entry [0x000000001f00f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
- waiting to lock <0x00000000d6d6baf8> (a java.lang.Object)
- locked <0x00000000d6d6bb08> (a java.lang.Object)
Aqui é visível que a thread “Thread-1” está bloqueada (BLOCKED), mantém um monitor, mas aguarda outro. Se você vir várias threads assim, em que uma mantém o recurso A e aguarda o B, e outra mantém o B e aguarda o A — isso é um deadlock clássico.
Estados de threads
| Status | Descrição |
|---|---|
| RUNNABLE | A thread está em execução ou pronta para executar |
| BLOCKED | Aguarda adquirir um monitor (lock) |
| WAITING | Aguarda notify()/notifyAll() (por exemplo, após chamar wait()) |
| TIMED_WAITING | Aguarda com timeout (por exemplo, sleep, wait(timeout)) |
| TERMINATED | Thread finalizada |
Importante: o status RUNNABLE nem sempre significa que a thread está executando neste exato momento — ela apenas está pronta para executar (o escalonador da JVM pode não iniciá-la imediatamente).
Como saber se há um deadlock?
No dump, várias threads em estado BLOCKED, cada uma aguardando um monitor mantido por outra thread do mesmo conjunto.
No final do dump, o jstack geralmente imprime:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000d6d6baf8 (object 0x00000000d6d6baf8, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00000000d6d6bb08 (object 0x00000000d6d6bb08, a java.lang.Object),
which is held by "Thread-1"
Se as threads permanecerem por muito tempo em BLOCKED ou WAITING — é motivo para investigação.
2. Monitoramento e perfilamento de threads
VisualVM
VisualVM — uma ferramenta gratuita, incluída na maioria dos JDKs. Permite conectar a um processo, ver o estado das threads, fazer um Thread Dump, visualizar o uso de CPU, threads ativas e “presas”.
Aba Threads: mostra quantas threads foram criadas, seus estados e o histórico de atividade.
Thread Dump: o botão “Thread Dump” tira um snapshot, análogo ao jstack.
Java Mission Control e Flight Recorder
Java Mission Control (JMC): ferramenta avançada para análise do funcionamento da JVM em tempo real. Ajuda a investigar locks, tempos de execução, alocações e latências.
Java Flight Recorder (JFR): profiler integrado da JVM, que coleta eventos sobre threads, locks, pausas etc.
Exemplo: monitoramento de locks
No VisualVM ou no JMC é possível ver que:
- A thread “A” está bloqueada no objeto X.
- A thread “B” detém o objeto X, mas aguarda o objeto Y.
- A thread “C” detém o objeto Y, mas aguarda o objeto X.
Este é um deadlock clássico em ciclo.
Como usar essas ferramentas na prática?
- Inicie o aplicativo com a flag -XX:+FlightRecorder (ou simplesmente use JDK 11+).
- Abra o JMC, conecte-se ao processo e inicie a gravação (start recording).
- Analise os hot spots, locks demorados e a concorrência entre threads.
3. Logging e rastreamento
Em programas multithread, depurar “no olho” dói. Registre a entrada/saída de seções críticas (blocos synchronized), operações com variáveis compartilhadas, esperas e sinais das threads — assim você entende quem e quando adquiriu ou liberou um recurso.
Como fazer logging?
- Use ferramentas padrão: java.util.logging, SLF4J, Log4j.
- Registre o nome da thread: Thread.currentThread().getName().
- Registre o horário e os identificadores das threads.
- Registre eventos de aquisição/liberação de locks.
Exemplo de logging
synchronized(lock) {
System.out.println(Thread.currentThread().getName() + " adquiriu o lock");
// seção crítica
System.out.println(Thread.currentThread().getName() + " saiu do lock");
}
Uso de nomes de threads
Dê nomes significativos às threads!
Thread t = new Thread(runnable, "MyWorker-1");
Exemplo de rastreamento com o logger
import java.util.logging.Logger;
public class Example {
private static final Logger logger = Logger.getLogger(Example.class.getName());
public void doWork() {
logger.info(Thread.currentThread().getName() + " começou a trabalhar");
synchronized (this) {
logger.info(Thread.currentThread().getName() + " entrou em synchronized");
// ...
}
logger.info(Thread.currentThread().getName() + " terminou o trabalho");
}
}
4. Boas práticas de diagnóstico
Minimize o escopo dos locks
Mantenha os locks pelo menor tempo possível.
Exemplo ruim:
synchronized(lock) {
// E/S demorada
// cálculos complexos
// acesso ao BD
// ... e só então trabalhar com dados compartilhados
}
Bom exemplo:
// fora do synchronized: E/S demorada, cálculos
synchronized(lock) {
// apenas trabalho com dados compartilhados
}
Use nomes de threads
Nomes significativos economizam tempo na análise de dumps e logs.
Escreva testes de multithreading
Use JUnit + CountDownLatch para modelar cenários concorrentes.
CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
// ...
latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // aguardamos a conclusão de ambas as threads
Use try-finally para ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// seção crítica
} finally {
lock.unlock();
}
Assim você não esquecerá de liberar o lock mesmo em caso de exceção. Para evitar deadlocks, use tryLock() com timeout.
Documente por que a sincronização é necessária
Comentários “Aqui precisa de synchronized, porque…” ajudam, com o tempo, a entender a intenção.
5. Prática: análise de deadlock em um programa de teste
Exemplo de código com deadlock
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1: adquiriu lockA");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockB) {
System.out.println("Thread-1: adquiriu lockB");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2: adquiriu lockB");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockA) {
System.out.println("Thread-2: adquiriu lockA");
}
}
}, "Thread-2");
t1.start();
t2.start();
}
}
Como capturar um deadlock
- Execute o programa — ele vai travar.
- Obtenha um thread dump (jstack ou via VisualVM).
- Encontre “Thread-1” e “Thread-2” — você verá que cada uma detém um lock e aguarda o outro.
- No final do dump haverá a seção “Found one Java-level deadlock”.
Como corrigir
- Adquira os locks sempre na mesma ordem.
- Use ReentrantLock com tryLock() e timeout: se não conseguir adquirir todos os locks, libere e tente novamente.
6. Erros comuns ao diagnosticar programas multithread
Erro nº 1: Não saber ler um thread dump. Desenvolvedores iniciantes se assustam com o dump: “Que rastros de pilha e status estranhos são esses?” Na prática, basta conhecer os principais status e procurar BLOCKED/WAITING para simplificar a análise.
Erro nº 2: Ignorar os nomes das threads. Sem nomes significativos, entender o dump é como procurar uma agulha no palheiro. Não deixe de dar nomes!
Erro nº 3: blocos synchronized grandes demais. Se você sincroniza partes grandes do código, as threads vão se bloquear com mais frequência — isso aparece como muitos BLOCKED no dump.
Erro nº 4: confundir RUNNABLE com uma thread realmente em execução. RUNNABLE nem sempre está “rodando” no processador. O escalonador da JVM decide quem executar.
Erro nº 5: não usar ferramentas de monitoramento. Muitos não conhecem o VisualVM, o JMC, o Flight Recorder e sofrem com println. Use as ferramentas — elas facilitam muito a vida.
Erro nº 6: ausência de logging de operações críticas. Sem logs, entender quem adquiriu/liberou o lock e quando é quase impossível.
Erro nº 7: tentar “no olho” capturar data races. Corridas de dados nem sempre se manifestam imediatamente — use testes com CountDownLatch, provoque concorrência via Thread.yield() e analise o estado das variáveis compartilhadas.
GO TO FULL VERSION