Deadlock (bloqueio mútuo) é um estado no qual duas ou mais threads aguardam indefinidamente pela liberação de recursos retidos por outros participantes. De acordo com Oracle Java Tutorials (2024), Deadlock ocorre em espera circular quando cada thread mantém um bloqueio necessário para outra thread. Sem ferramentas especiais de detecção, Deadlock interrompe completamente a execução do aplicativo sem erros visíveis.
Principais pontos
Deadlock é uma situação em programação multithread onde duas ou mais threads bloqueiam permanentemente umas às outras. Cada thread retém um recurso necessário para outra thread e não o libera enquanto espera para adquirir o recurso ausente. Como resultado, nenhuma das threads pode continuar a execução.
No desenvolvimento móvel, Deadlock é especialmente crítico porque não causa exceções ou falhas. O aplicativo simplesmente para de responder às ações do usuário (ANR — Application Not Responding), e a única saída é forçar o encerramento do processo. De acordo com Google (Android Performance Patterns, 2023), cerca de 15% dos relatórios ANR no Google Play Console estão relacionados a bloqueios mútuos em threads em segundo plano.
A principal diferença entre Deadlock e outros problemas de concorrência é sua irreversibilidade sem intervenção externa. As threads não liberarão recursos por conta própria porque o escalonador do sistema operacional não pode revogar um bloqueio à força. Isso distingue Deadlock de Livelock, onde as threads estão ativas mas não realizam trabalho útil.
Em 1971, Edward G. Coffman formulou quatro condições obrigatórias necessárias para ocorrer Deadlock. Se pelo menos uma delas estiver ausente, o bloqueio mútuo é impossível. Essas condições são conhecidas como condições de Coffman e formam a base de todos os algoritmos de prevenção de Deadlock.
Um recurso pode ser adquirido por apenas uma thread a cada momento. Se um recurso permite leitura simultânea por várias threads (por exemplo, ReadWriteLock em modo de leitura), Deadlock não ocorre. Esta condição decorre da própria natureza do Mutex e dos bloqueios.
Uma thread retém um recurso já adquirido e simultaneamente aguarda adquirir outro recurso. Se uma thread pode liberar o recurso atual antes de solicitar o próximo (através de bloqueio de duas fases), a condição Hold and Wait é quebrada. No Android, isso frequentemente se manifesta quando uma thread mantém um bloqueio de banco de dados e tenta adquirir um bloqueio de SharedPreferences.
O sistema operacional não pode retirar à força um bloqueio de uma thread. O recurso é liberado apenas quando a thread mesma o libera. Em alguns sistemas (por exemplo, SQLite em modo WAL), a preempção forçada é implementada no nível de operações individuais, o que reduz o risco de Deadlock.
Existe uma cadeia fechada de threads, cada uma aguardando um recurso retido pela próxima na cadeia. Por exemplo, a thread A retém o recurso 1 e aguarda o recurso 2, a thread B retém o recurso 2 e aguarda o recurso 1. Esta é a única condição que um desenvolvedor pode eliminar arquiteturalmente — através de uma hierarquia de bloqueios. Se todas as threads adquirem recursos em uma ordem global estritamente definida, um ciclo é fisicamente impossível.
Na prática, em aplicativos Android, Deadlock ocorre mais frequentemente devido à interseção implícita de bloqueios de diferentes níveis: bloqueio de banco de dados (Room), bloqueio de SharedPreferences e bloqueio de coleção em memória. Cada um desses bloqueios é gerenciado por diferentes componentes, e sem um protocolo centralizado de ordem de aquisição, os desenvolvedores criam ciclos involuntariamente.
Vamos considerar um exemplo clássico de bloqueio mútuo — duas threads adquirem bloqueios em ordem diferente. Se a primeira thread bloqueia o recurso A e tenta adquirir B, e a segunda bloqueia B e tenta adquirir A, ocorre Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // simulação de trabalho
synchronized(lockB) {
println("operationA concluída")
}
}
}
fun operationB() {
synchronized(lockB) { // ordem inversa
Thread.sleep(50)
synchronized(lockA) {
println("operationB concluída")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// O aplicativo travará para sempre — Deadlock!
}
Neste exemplo, operationA adquire lockA, e operationB adquire lockB. Então cada uma tenta adquirir o segundo bloqueio — e ambas aguardam indefinidamente. O programa trava sem uma mensagem de erro. A única maneira de corrigir é garantir a mesma ordem de aquisição de bloqueios em todos os métodos.
Esses três problemas de concorrência são frequentemente confundidos, mas seus mecanismos e consequências são fundamentalmente diferentes. Deadlock — parada completa, Starvation — espera infinita por um recurso, Livelock — inatividade ativa. Compreender as diferenças é criticamente importante para escolher a estratégia de resolução correta.
| Característica | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Estado das threads | Bloqueadas (BLOCKED) | Prontas (RUNNABLE) | Ativas (RUNNABLE) |
| Trabalho sendo feito | Não | Não | Sim, mas inútil |
| Causa | Espera circular | Escalonamento injusto | Tratamento incorreto de conflitos |
| Detecção | Thread Dump, timeouts | Monitoramento de progresso | Contador de tentativas |
Starvation (inanição) ocorre quando o escalonador adia constantemente a execução de uma thread de baixa prioridade em favor de outras. Ao contrário de Deadlock, a thread não está bloqueada — está pronta para executar mas não recebe tempo de CPU. No Android, um cenário típico é uma thread em segundo plano de baixa prioridade que nunca executa se a thread de UI e as threads de Service estão constantemente ativas.
Livelock é uma situação onde as threads não estão bloqueadas mas reagem infinitamente às ações umas das outras sem realizar trabalho útil. A analogia clássica — duas pessoas se encontram em um corredor e ambas tentam ceder passagem, movendo-se na mesma direção. Ao contrário de Deadlock, threads em Livelock consomem CPU, drenando a bateria do dispositivo.
Thread Dump é a principal ferramenta para detectar bloqueios mútuos na JVM e Android Runtime. Durante um dump, a JVM analisa automaticamente o gráfico de dependências entre monitores e marca os ciclos de Deadlock. No Android Studio, dumps de thread podem ser obtidos através do Android Profiler ou do comando kill -3 PID do ADB Shell.
A detecção automática de Deadlock em tempo de execução é implementada através de temporizadores Watchdog. Se uma thread não concluir uma operação dentro de um tempo limite especificado, o watchdog inicia um dump e envia um relatório para o sistema de Crash Reporting (Firebase Crashlytics, Sentry). De acordo com Sentry (Issue Resolution Report, 2024), configurar um watchdog reduz o tempo de diagnóstico de Deadlock de semanas para algumas horas.
Durante o desenvolvimento, são eficazes o analisador estático ThreadSafe da JetBrains e o Checker Framework com o módulo Lock Checker. Essas ferramentas analisam a ordem de aquisição de bloqueios no nível do código fonte e alertam sobre potenciais ciclos. Adicionalmente, recomenda-se Test-Driven Deadlock Detection — testes de estresse que executam operações com diferentes ordens de bloqueio em centenas de threads.
Atenção especial merece a Cooperative Deadlock Detection — um método onde as threads trocam informações sobre bloqueios adquiridos através de um registro global. Se uma thread detecta um ciclo potencial, ela libera todos os recursos e tenta a operação novamente. Esta abordagem é usada em sistemas distribuídos (Apache ZooKeeper, Google Chubby) e está sendo gradualmente adotada no desenvolvimento móvel através de bibliotecas como Jetpack Sync.
A maneira mais confiável é estabelecer uma ordem global de aquisição de bloqueios em todo o aplicativo. Se todas as threads sempre adquirem primeiro o bloqueio com o número menor, depois o de número maior, a espera circular (condição Circular Wait) é impossível. Em projetos grandes, a ordem é documentada e verificada através de code review.
TryLock é um método de bloqueio que não bloqueia uma thread indefinidamente, mas retorna false se o bloqueio não for adquirido dentro de um tempo especificado. Em Java, isso é implementado via ReentrantLock.tryLock(timeout, TimeUnit), em Kotlin Coroutines — via Mutex.withLock com tempo limite. Em caso de falha, a thread libera todos os recursos adquiridos e tenta novamente mais tarde.
Algoritmo do banqueiro é um método teórico de prevenção de Deadlock proposto por Edsger Dijkstra. Ele modela a alocação de recursos como transações bancárias: o sistema não aloca um recurso se isso puder levar a um estado inseguro (deadlock). Na prática, o algoritmo raramente é usado no desenvolvimento móvel devido à dificuldade de conhecer antecipadamente as necessidades máximas das threads, mas seus princípios são usados em bancos de dados SQLite e sistemas de arquivos.
Perguntas frequentes
Não, o bloqueio mútuo requer pelo menos duas threads. Em código de thread única, todas as operações são executadas sequencialmente, então a espera circular é impossível. No entanto, Deadlock pode ocorrer entre processos ao usar bloqueios de arquivo ou semáforos entre processos.
Em corrotinas, Deadlock ocorre no nível de funções suspend e não bloqueia a thread do SO, tornando-o menos perceptível. Mutex do kotlinx.coroutines é um bloqueio suspensivo (suspending) — não bloqueia a thread, mas a corrotina não executa. Para detecção, use DebugProbes do módulo kotlinx-coroutines-debug.
Deadlock no SQLite ocorre quando duas conexões com o banco de dados tentam executar transações em ordens diferentes. SQLite detecta tais situações e retorna o código de erro SQLITE_BUSY ou SQLITE_LOCKED. No Android, recomenda-se usar Room com uma única instância de banco de dados e transações via @Transaction, o que elimina Deadlock entre conexões.
Android Runtime possui um detector de Deadlock embutido que é executado quando um ANR (Application Not Responding) é gerado. O sistema analisa o Thread Dump de todas as threads do aplicativo e marca os bloqueios mútuos. O resultado está disponível em /data/anr/traces.txt e no Google Play Console na seção ANR Reports.
Primeiro, obtenha um Thread Dump de todas as threads do aplicativo. Analise quais bloqueios cada thread retém e quais está tentando adquirir. Implemente um temporizador Watchdog com dump automático quando o limite de tempo for excedido. Após a correção, adicione a regra lint ThreadSafety ao pipeline de CI para prevenir recorrências.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também