Starvation (inanição de thread) — é uma situação em que uma thread não consegue acessar um recurso necessário para continuar seu trabalho, embora esteja pronta para execução. De acordo com Baeldung (Java Thread Starvation, 2024), a inanição surge devido a um escalonamento injusto, onde threads de baixa prioridade são constantemente adiadas em favor das de maior prioridade. Ao contrário de Deadlock, Starvation não bloqueia a thread — ela permanece no estado RUNNABLE, mas nunca recebe tempo de CPU.
Pontos Principais
Starvation (inanição de thread) — é um problema de programação multithread onde uma thread não consegue acessar um recurso necessário para completar sua tarefa, embora o recurso não esteja permanentemente bloqueado por outra thread. A thread está no estado RUNNABLE, mas o escalonador ou mecanismo de sincronização adia sistematicamente sua execução em favor de outras threads.
No desenvolvimento móvel, Starvation manifesta-se como uma execução desigual de tarefas: algumas operações executam instantaneamente, enquanto outras sofrem atrasos catastróficos. Por exemplo, uma thread de sincronização de dados em segundo plano pode nunca obter acesso ao banco de dados se a thread da UI e os manipuladores de animação a precederem constantemente. De acordo com o Android Developer Blog (Performance Matters, 2023), cerca de 12% dos quadros perdidos (jank) no Android são causados por Starvation de tarefas em segundo plano das quais a renderização depende.
A principal diferença entre Starvation e Deadlock é a reversibilidade. Se a carga do sistema diminuir ou as prioridades forem redistribuídas, a thread faminta pode adquirir o recurso e completar seu trabalho. No entanto, sob carga alta sustentada, Starvation pode durar indefinidamente, criando a impressão de uma aplicação congelada.
synchronized em Java e Kotlin é um exemplo clássico de mecanismo injusto. Sob alta contenção, a JVM pode conceder continuamente o bloqueio às mesmas threads ativas, enquanto outras threads perdem constantemente a disputa. Isso não é um bug da JVM, mas uma troca de design: bloqueios injustos fornecem maior throughput em detrimento da justiça de acesso. Para aplicações móveis com 4–8 threads, este problema é especialmente relevante.
Definir prioridades diferentes de threads pode levar à Starvation de threads de baixa prioridade. No Android Runtime, o escalonador CFS (Completely Fair Scheduler) do Linux distribui o tempo de CPU proporcionalmente às prioridades, e se threads de alta prioridade estiverem constantemente ativas, threads de baixa prioridade podem nunca receber tempo de CPU. O Google desencoraja fortemente alterar prioridades de threads no Android — o sistema as gerencia por si só.
Se uma thread mantém um bloqueio por muito tempo (realizando cálculos pesados, requisições de rede ou operações de arquivo dentro de um bloco synchronized), outras threads esperando por esse bloqueio sofrem inanição. Isso é especialmente perigoso no Android, onde operações longas na thread da UI causam ANR, e movê-las para threads em segundo plano sem otimizar as seções críticas apenas transfere o problema de Starvation para as threads trabalhadoras.
Considere um exemplo onde uma thread adquire um bloqueio com muita frequência devido a um escalonamento injusto. Starvation é demonstrada através de um loop infinito de uma thread de alta prioridade que impede uma thread de baixa prioridade de acessar um recurso compartilhado.
class SharedResource {
private val lock = Any()
fun criticalSection(id: String) {
synchronized(lock) {
println("$id obteve acesso")
Thread.sleep(10) // simulando trabalho
}
}
}
fun main() {
val resource = SharedResource()
// Thread de alta prioridade — ativa constantemente
val highPriority = Thread {
while (true) {
resource.criticalSection("High")
}
}
// Thread de baixa prioridade — pode nunca obter acesso
val lowPriority = Thread {
while (true) {
resource.criticalSection("Low")
}
}
highPriority.start()
lowPriority.start()
// "Low" pode nunca imprimir uma mensagem — Starvation!
}
Neste exemplo, a thread highPriority adquire constantemente o bloqueio e o libera por apenas 10 ms. Devido à natureza injusta do synchronized, o escalonador da JVM muito provavelmente concederá o bloqueio novamente à mesma thread que acabou de liberá-lo — a thread de baixa prioridade sofre inanição. A solução é usar ReentrantLock(true) com a flag fair, que garante uma ordem FIFO de espera.
A versão corrigida com um bloqueio justo assegura uma distribuição equitativa do acesso ao recurso.
class FairSharedResource {
private val fairLock = ReentrantLock(true) // fair = true
fun criticalSection(id: String) {
fairLock.lock()
try {
println("$id obteve acesso (fair)")
Thread.sleep(10)
} finally {
fairLock.unlock()
}
}
}
Três problemas clássicos de multithreading — Starvation, Deadlock e Livelock — são frequentemente agrupados, mas seus mecanismos e soluções diferem. Starvation — a thread está pronta mas não consegue adquirir o recurso. Deadlock — as threads estão bloqueadas por espera cíclica. Livelock — as threads estão ativas mas não progridem.
| Parâmetro | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Estado da thread | RUNNABLE | BLOCKED | RUNNABLE |
| Progresso | Nenhum | Nenhum | Nenhum (embora ativo) |
| Uso de CPU | Baixo | Mínimo | Alto (até 100%) |
| Causa | Escalonamento injusto | Espera cíclica | Mesma resposta ao conflito |
| Correção principal | Fair Lock, encurtar seções críticas | Hierarquia de bloqueios | Limite de tentativas, exponential backoff |
Starvation é considerado menos crítico que Deadlock porque não é fatal — sob carga reduzida, a thread faminta eventualmente executará. No entanto, no uso real do Android, onde memória e CPU são limitados, Starvation pode durar minutos, criando uma experiência de usuário inaceitável.
Thread Dump feito repetidamente em intervalos curtos — o método básico para detectar inanição. Se uma thread está consistentemente no estado RUNNABLE mas sua pilha de chamadas não muda através de múltiplos dumps — este é um sinal clássico de Starvation. No Android Studio, use o Android Profiler com gravação do estado das threads ao longo do tempo.
A detecção automatizada é possível através do monitoramento do tempo de execução de tarefas. Se uma tarefa com um tempo de execução previsível (por exemplo, 50 ms) leva 5 segundos ou mais — há uma alta probabilidade de Starvation. Em aplicações móveis, o Firebase Performance Monitoring permite configurar traces personalizados para seções críticas e receber notificações quando limites são excedidos.
Para diagnosticar Starvation causada por blocos synchronized, use Java Flight Recorder (JFR) (disponível no Android através da API OpenJDK) ou Async Profiler. Estas ferramentas mostram quais monitores têm os maiores tempos de espera e quais threads competem por cada monitor. Os dados do JFR integram-se com IntelliJ IDEA Ultimate através do seu perfilador integrado.
ReentrantLock(true) garante que as threads adquiram o bloqueio em ordem FIFO. Ao contrário do synchronized, um bloqueio justo não permite que uma thread que acabou de liberar o bloqueio o readquira imediatamente. Isso elimina completamente Starvation, embora reduza o throughput geral em 10–20% devido à sobrecarga de manter a fila.
Estruturas de dados sem bloqueios (ConcurrentHashMap, AtomicReference, LongAdder) eliminam Starvation por definição, pois não possuem bloqueios que possam ser retidos por uma thread. Todas as operações usam instruções CAS da CPU que garantem que pelo menos uma thread progrida em um número finito de passos. Para desenvolvimento móvel, prefira ConcurrentLinkedQueue para filas de tarefas.
Minimizar o tempo de retenção do bloqueio é uma forma universal de reduzir o risco de Starvation. Mova operações pesadas (rede, E/S de disco, cálculos complexos) para fora dos blocos synchronized. Use ReadWriteLock para cenários onde leitores não devem sofrer inanição devido a escritores infrequentes. A biblioteca Kotlin Coroutines fornece Mutex com um mecanismo de suspensão que não bloqueia uma thread do SO.
Condition.await() e signal() devem ser usados com cuidado: uma thread esperando em uma Condition acorda junto com outras threads (spurious wakeup), e todas competem pelo bloqueio. Se uma thread retorna imediatamente à espera após await enquanto outras conseguem adquirir o bloqueio, a thread faminta pode acordar e dormir indefinidamente. Sempre verifique a condição em um loop while em vez de if para garantir a re-verificação.
Perguntas Frequentes
Priority Inversion — é uma situação onde uma thread de baixa prioridade mantém um bloqueio necessário para uma thread de alta prioridade. Como resultado, a thread de alta prioridade espera pela de baixa prioridade — as prioridades são invertidas. Starvation é um problema mais amplo: uma thread não consegue adquirir um recurso independentemente da prioridade, devido a escalonamento injusto ou seções críticas longas.
Não, Starvation é um problema de multithreading. Código single-thread não tem contenção de recursos ou escalonamento de threads. No entanto, Starvation pode ocorrer em código assíncrono single-thread (por exemplo, event loop do JavaScript) se uma microtarefa adiar indefinidamente a execução de outras através de setTimeout com atraso zero.
JMM (Java Memory Model) define regras para visibilidade de mudanças entre threads mas não garante escalonamento justo. synchronized, de acordo com JMM, assegura consistência sequencial — correção básica — mas não previne Starvation. Justiça requer mecanismos adicionais não especificados na JMM.
A thread de UI (Main Thread) não pode sofrer inanição no sentido clássico porque tem a mais alta prioridade. No entanto, Starvation ocorre quando a thread de UI espera por um resultado de uma thread de fundo faminta. Um cenário típico: uma AsyncTask ou corrotina carrega dados mas não consegue acessar o banco de dados devido à contenção com outras threads, e a UI congela enquanto espera.
Em corrotinas, para prevenir Starvation, use limitedParallelism no Dispatchers.IO para evitar esgotamento de threads. Para sincronização, use Mutex de kotlinx.coroutines.sync — ele suspende a corrotina em vez de bloquear a thread, reduzindo o risco de inanição. Evite runBlocking em corrotinas, pois ele pode capturar uma thread do pool e causar Starvation de outras corrotinas.
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