CodeGym /Cursos /JAVA 25 SELF /Diagnóstico e depuração de programas multithread

Diagnóstico e depuração de programas multithread

JAVA 25 SELF
Nível 53 , Lição 4
Disponível

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

  1. Execute o programa — ele vai travar.
  2. Obtenha um thread dump (jstack ou via VisualVM).
  3. Encontre “Thread-1” e “Thread-2” — você verá que cada uma detém um lock e aguarda o outro.
  4. 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.

1
Pesquisa/teste
Problemas de multithreading, nível 53, lição 4
Indisponível
Problemas de multithreading
Problemas de multithreading
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION