Starvation (inanición de hilo) es una situación en la que un hilo no puede acceder a un recurso necesario para continuar su trabajo, aunque esté listo para ejecutarse. Según Baeldung (Java Thread Starvation, 2024), la inanición surge debido a una planificación injusta, donde los hilos de baja prioridad se postergan constantemente en favor de los de mayor prioridad. A diferencia de Deadlock, Starvation no bloquea el hilo — permanece en estado RUNNABLE pero nunca obtiene tiempo de CPU.
Puntos Clave
Starvation (inanición de hilo) es un problema de programación multihilo donde un hilo no puede acceder a un recurso necesario para completar su tarea, aunque el recurso no esté bloqueado permanentemente por otro hilo. El hilo está en estado RUNNABLE, pero el planificador o el mecanismo de sincronización pospone sistemáticamente su ejecución en favor de otros hilos.
En el desarrollo móvil, Starvation se manifiesta como una ejecución desigual de tareas: algunas operaciones se ejecutan al instante, mientras que otras sufren retrasos catastróficos. Por ejemplo, un hilo de fondo de sincronización de datos puede nunca obtener acceso a la base de datos si el hilo de la UI y los manejadores de animaciones lo adelantan constantemente. Según Android Developer Blog (Performance Matters, 2023), aproximadamente el 12% de los fotogramas perdidos (jank) en Android son causados por Starvation de tareas en segundo plano de las que depende el renderizado.
La diferencia clave entre Starvation y Deadlock es la reversibilidad. Si la carga del sistema disminuye o las prioridades se redistribuyen, el hilo en inanición puede adquirir el recurso y completar su trabajo. Sin embargo, bajo una carga alta sostenida, Starvation puede durar indefinidamente, creando la impresión de una aplicación congelada.
synchronized en Java y Kotlin es un ejemplo clásico de mecanismo injusto. Bajo alta contención, la JVM puede otorgar continuamente el bloqueo a los mismos hilos activos, mientras que otros hilos pierden constantemente la carrera. Esto no es un error de la JVM sino una compensación de diseño: los bloqueos injustos proporcionan mayor rendimiento a expensas de la equidad de acceso. Para aplicaciones móviles con 4–8 hilos, este problema es especialmente relevante.
Establecer diferentes prioridades de hilos puede provocar Starvation de los hilos de baja prioridad. En Android Runtime, el planificador CFS (Completely Fair Scheduler) de Linux distribuye el tiempo de CPU proporcionalmente a las prioridades, y si los hilos de alta prioridad están constantemente activos, los de baja prioridad pueden nunca obtener tiempo de CPU. Google desaconseja firmemente cambiar las prioridades de los hilos en Android — el sistema las gestiona por sí mismo.
Si un hilo mantiene un bloqueo durante demasiado tiempo (realizando cálculos pesados, solicitudes de red u operaciones de archivo dentro de un bloque synchronized), otros hilos que esperan ese bloqueo sufren inanición. Esto es especialmente peligroso en Android, donde las operaciones largas en el hilo de la UI causan ANR, y moverlas a hilos en segundo plano sin optimizar las secciones críticas solo transfiere el problema de Starvation a los hilos trabajadores.
Considere un ejemplo donde un hilo adquiere un bloqueo con demasiada frecuencia debido a una planificación injusta. Starvation se demuestra mediante un bucle infinito de un hilo de alta prioridad que impide que un hilo de baja prioridad acceda a un recurso compartido.
class SharedResource {
private val lock = Any()
fun criticalSection(id: String) {
synchronized(lock) {
println("$id obtuvo acceso")
Thread.sleep(10) // simulando trabajo
}
}
}
fun main() {
val resource = SharedResource()
// Hilo de alta prioridad — activo constantemente
val highPriority = Thread {
while (true) {
resource.criticalSection("High")
}
}
// Hilo de baja prioridad — puede nunca obtener acceso
val lowPriority = Thread {
while (true) {
resource.criticalSection("Low")
}
}
highPriority.start()
lowPriority.start()
// "Low" puede nunca imprimir un mensaje — ¡Starvation!
}
En este ejemplo, el hilo highPriority adquiere constantemente el bloqueo y lo libera solo por 10 ms. Debido a la naturaleza injusta de synchronized, el planificador de la JVM probablemente otorgará el bloqueo nuevamente al mismo hilo que acaba de liberarlo — el hilo de baja prioridad sufre inanición. La solución es usar ReentrantLock(true) con el indicador fair, que garantiza un orden FIFO de espera.
La versión corregida con un bloqueo justo asegura una distribución equitativa del acceso al recurso.
class FairSharedResource {
private val fairLock = ReentrantLock(true) // fair = true
fun criticalSection(id: String) {
fairLock.lock()
try {
println("$id obtuvo acceso (fair)")
Thread.sleep(10)
} finally {
fairLock.unlock()
}
}
}
Tres problemas clásicos de multihilo — Starvation, Deadlock y Livelock — a menudo se agrupan, pero sus mecanismos y soluciones difieren. Starvation — el hilo está listo pero no puede adquirir el recurso. Deadlock — los hilos están bloqueados por espera cíclica. Livelock — los hilos están activos pero no progresan.
| Parámetro | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Estado del hilo | RUNNABLE | BLOCKED | RUNNABLE |
| Progreso | Ninguno | Ninguno | Ninguno (aunque activo) |
| Uso de CPU | Bajo | Mínimo | Alto (hasta 100%) |
| Causa | Planificación injusta | Espera cíclica | Misma respuesta al conflicto |
| Solución principal | Fair Lock, acortar secciones críticas | Jerarquía de bloqueos | Límite de reintentos, exponential backoff |
Starvation se considera menos crítico que Deadlock porque no es fatal — bajo carga reducida, el hilo en inanición eventualmente se ejecutará. Sin embargo, en el uso real de Android, donde la memoria y la CPU son limitadas, Starvation puede durar minutos, creando una experiencia de usuario inaceptable.
Thread Dump tomado repetidamente a intervalos cortos — el método básico para detectar inanición. Si un hilo está consistentemente en estado RUNNABLE pero su pila de llamadas no cambia a través de múltiples volcados — esta es una señal clásica de Starvation. En Android Studio, use Android Profiler con grabación del estado de los hilos a lo largo del tiempo.
La detección automatizada es posible mediante monitoreo del tiempo de ejecución de tareas. Si una tarea con un tiempo de ejecución predecible (por ejemplo, 50 ms) tarda 5 segundos o más — hay una alta probabilidad de Starvation. En aplicaciones móviles, Firebase Performance Monitoring permite configurar trazas personalizadas para secciones críticas y recibir notificaciones cuando se superan los umbrales.
Para diagnosticar Starvation causada por bloques synchronized, use Java Flight Recorder (JFR) (disponible en Android a través de la API de OpenJDK) o Async Profiler. Estas herramientas muestran qué monitores tienen los tiempos de espera más altos y qué hilos compiten por cada monitor. Los datos de JFR se integran con IntelliJ IDEA Ultimate a través de su perfilador integrado.
ReentrantLock(true) garantiza que los hilos adquieran el bloqueo en orden FIFO. A diferencia de synchronized, un bloqueo justo no permite que un hilo que acaba de liberar el bloqueo lo readquiera inmediatamente. Esto elimina completamente Starvation, aunque reduce el rendimiento general entre un 10–20% debido a la sobrecarga de mantener la cola.
Estructuras de datos sin bloqueos (ConcurrentHashMap, AtomicReference, LongAdder) eliminan Starvation por definición, ya que no tienen bloqueos que puedan ser retenidos por un hilo. Todas las operaciones usan instrucciones CAS de la CPU que garantizan que al menos un hilo progrese en un número finito de pasos. Para desarrollo móvil, prefiera ConcurrentLinkedQueue para colas de tareas.
Minimizar el tiempo de retención del bloqueo es una forma universal de reducir el riesgo de Starvation. Mueva operaciones pesadas (red, E/S de disco, cálculos complejos) fuera de los bloques synchronized. Use ReadWriteLock para escenarios donde los lectores no deben sufrir inanición debido a escritores poco frecuentes. La biblioteca Kotlin Coroutines proporciona Mutex con un mecanismo de suspensión que no bloquea un hilo del sistema operativo.
Condition.await() y signal() deben usarse con precaución: un hilo esperando en una Condition se despierta junto con otros hilos (spurious wakeup), y todos compiten por el bloqueo. Si un hilo regresa inmediatamente a espera después de await mientras otros logran adquirir el bloqueo, el hilo en inanición puede despertarse y volver a dormirse indefinidamente. Siempre verifique la condición en un bucle while en lugar de un if para garantizar la re-verificación.
Preguntas Frecuentes
Priority Inversion es una situación en la que un hilo de baja prioridad mantiene un bloqueo necesario para un hilo de alta prioridad. Como resultado, el hilo de alta prioridad espera al de baja prioridad — las prioridades se invierten. Starvation es un problema más amplio: un hilo no puede adquirir un recurso independientemente de la prioridad, debido a una planificación injusta o secciones críticas largas.
No, Starvation es un problema de multihilo. El código de un solo hilo no tiene contención de recursos ni planificación de hilos. Sin embargo, Starvation puede ocurrir en código asíncrono de un solo hilo (por ejemplo, el bucle de eventos de JavaScript) si una microtarea pospone indefinidamente la ejecución de otras mediante setTimeout con retardo cero.
JMM (Java Memory Model) define reglas para la visibilidad de cambios entre hilos pero no garantiza una planificación justa. synchronized, de acuerdo con JMM, asegura consistencia secuencial — corrección básica — pero no previene Starvation. La equidad requiere mecanismos adicionales no especificados en JMM.
El hilo de UI (Main Thread) no puede sufrir inanición en el sentido clásico porque tiene la prioridad más alta. Sin embargo, Starvation ocurre cuando el hilo de UI espera un resultado de un hilo de fondo en inanición. Un escenario típico: un AsyncTask o corutina carga datos pero no puede acceder a la base de datos debido a contención con otros hilos, y la UI se congela mientras espera.
En corutinas, para prevenir Starvation, use limitedParallelism en Dispatchers.IO para evitar el agotamiento de hilos. Para sincronización, use Mutex de kotlinx.coroutines.sync — suspende la corutina en lugar de bloquear el hilo, reduciendo el riesgo de inanición. Evite runBlocking en corutinas, ya que puede capturar un hilo del pool y causar Starvation de otras corutinas.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también