Starvation em aplicações móveis — essência, causas e formas de prevenir a inanição de thread

Autor: IT Sectr Publicado: 2026-03-18 Tempo de leitura: 10 min

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 — situação em que uma thread não consegue acessar um recurso apesar de estar pronta para execução
  • Diferente de Deadlock, uma thread em inanição permanece no estado RUNNABLE — não está bloqueada, mas não progride
  • Escalonamento injusto (por exemplo, sincronização via synchronized) — a principal causa de Starvation na JVM
  • Fair Lock (ReentrantLock(true)) garante uma ordem FIFO justa de aquisição de bloqueio
  • Thread Priority no desenvolvimento móvel é recomendado não alterar — o Android Runtime gerencia as prioridades por si só

O que é Starvation?

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.

Causas da inanição de thread

Bloqueios injustos (Non-Fair Locks)

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.

Uso incorreto de prioridades

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ções críticas longas

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.

Exemplo de Starvation em código Kotlin

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.

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

kotlin
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()
        }
    }
}

Starvation vs Deadlock vs Livelock

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âmetroStarvationDeadlockLivelock
Estado da threadRUNNABLEBLOCKEDRUNNABLE
ProgressoNenhumNenhumNenhum (embora ativo)
Uso de CPUBaixoMínimoAlto (até 100%)
CausaEscalonamento injustoEspera cíclicaMesma resposta ao conflito
Correção principalFair Lock, encurtar seções críticasHierarquia de bloqueiosLimite 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.

Como detectar Starvation

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.

Métodos para prevenir a inanição de thread

Fair Lock (ReentrantLock com flag true)

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 atômicas sem bloqueios

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.

Seções críticas curtas

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.

Variáveis de condição e sinais

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

Qual a diferença entre Starvation e Inversão de Prioridade (Priority Inversion)?

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.

Starvation pode ocorrer em uma aplicação single-thread?

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.

Como o Modelo de Memória Java se relaciona com Starvation?

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.

O que é Starvation na thread de UI do Android?

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.

Como prevenir Starvation em Kotlin Coroutines?

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

  • Starvation — situação em que uma thread está pronta para executar mas não consegue adquirir um recurso devido a escalonamento injusto
  • Diferente de Deadlock, uma thread faminta permanece no estado RUNNABLE e pode executar quando a carga diminui
  • Bloqueios injustos (synchronized) e uso incorreto de prioridades são as principais causas de Starvation
  • Fair Lock (ReentrantLock com flag true) garante ordem de acesso FIFO e elimina completamente a inanição
  • Estruturas sem bloqueios (ConcurrentHashMap, AtomicReference) eliminam Starvation no nível da arquitetura
  • Thread Dump com capturas repetidas e Java Flight Recorder são métodos eficazes para diagnosticar Starvation
  • Seções críticas curtas e ReadWriteLock reduzem a probabilidade de inanição em sistemas de alta carga

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.

Discutir o projeto

Leia também