CodeGym /Cursos /JAVA 25 SELF /Deadlock: causas, exemplos, eliminação

Deadlock: causas, exemplos, eliminação

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

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”.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION