Deadlock (bloqueo mutuo) es un estado en el que dos o más hilos esperan indefinidamente la liberación de recursos retenidos por otros participantes. Según Oracle Java Tutorials (2024), Deadlock ocurre en una espera circular cuando cada hilo mantiene un bloqueo necesario para otro hilo. Sin herramientas especiales de detección, Deadlock detiene por completo la ejecución de la aplicación sin errores visibles.
Puntos clave
Deadlock es una situación en programación multihilo donde dos o más hilos se bloquean permanentemente entre sí. Cada hilo retiene un recurso necesario para otro hilo y no lo libera mientras espera adquirir el recurso faltante. Como resultado, ninguno de los hilos puede continuar su ejecución.
En el desarrollo móvil, Deadlock es especialmente crítico porque no genera excepciones ni fallos. La aplicación simplemente deja de responder a las acciones del usuario (ANR — Application Not Responding), y la única salida es forzar la terminación del proceso. Según Google (Android Performance Patterns, 2023), aproximadamente el 15% de los informes ANR en Google Play Console están relacionados con bloqueos mutuos en hilos en segundo plano.
La diferencia clave entre Deadlock y otros problemas de concurrencia es su irreversibilidad sin intervención externa. Los hilos no liberarán recursos por sí mismos porque el planificador del sistema operativo no puede revocar forzosamente un bloqueo. Esto distingue a Deadlock de Livelock, donde los hilos están activos pero no realizan trabajo útil.
En 1971, Edward G. Coffman formuló cuatro condiciones obligatorias necesarias para que ocurra Deadlock. Si al menos una de ellas está ausente, el bloqueo mutuo es imposible. Estas condiciones se conocen como condiciones de Coffman y forman la base de todos los algoritmos de prevención de Deadlock.
Un recurso puede ser adquirido por solo un hilo en cada momento. Si un recurso permite la lectura simultánea por varios hilos (por ejemplo, ReadWriteLock en modo lectura), Deadlock no ocurre. Esta condición proviene de la propia naturaleza de Mutex y los bloqueos.
Un hilo retiene un recurso ya adquirido y simultáneamente espera adquirir otro recurso. Si un hilo puede liberar el recurso actual antes de solicitar el siguiente (mediante bloqueo de dos fases), la condición Hold and Wait se rompe. En Android, esto a menudo se manifiesta cuando un hilo retiene un bloqueo de base de datos e intenta adquirir un bloqueo de SharedPreferences.
El sistema operativo no puede quitar forzosamente un bloqueo a un hilo. El recurso se libera solo cuando el hilo mismo lo libera. En algunos sistemas (por ejemplo, SQLite en modo WAL), se implementa desalojo forzoso a nivel de operaciones individuales, lo que reduce el riesgo de Deadlock.
Existe una cadena cerrada de hilos, cada uno de los cuales espera un recurso retenido por el siguiente en la cadena. Por ejemplo, el hilo A retiene el recurso 1 y espera el recurso 2, el hilo B retiene el recurso 2 y espera el recurso 1. Esta es la única condición que un desarrollador puede eliminar arquitectónicamente — mediante una jerarquía de bloqueos. Si todos los hilos adquieren recursos en un orden global estrictamente definido, un ciclo es físicamente imposible.
En la práctica, en aplicaciones Android, Deadlock ocurre con mayor frecuencia debido a la intersección implícita de bloqueos de diferentes niveles: bloqueo de base de datos (Room), bloqueo de SharedPreferences y bloqueo de colección en memoria. Cada uno de estos bloqueos es gestionado por diferentes componentes, y sin un protocolo centralizado de orden de adquisición, los desarrolladores crean ciclos sin intención.
Consideremos un ejemplo clásico de bloqueo mutuo — dos hilos adquieren bloqueos en diferente orden. Si el primer hilo bloquea el recurso A e intenta adquirir B, y el segundo bloquea B e intenta adquirir A, ocurre Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // simulación de trabajo
synchronized(lockB) {
println("operationA completada")
}
}
}
fun operationB() {
synchronized(lockB) { // orden inverso
Thread.sleep(50)
synchronized(lockA) {
println("operationB completada")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// ¡La aplicación se colgará para siempre — Deadlock!
}
En este ejemplo, operationA adquiere lockA, y operationB adquiere lockB. Luego cada uno intenta adquirir el segundo bloqueo — y ambos esperan indefinidamente. El programa se cuelga sin un mensaje de error. La única forma de solucionarlo es garantizar el mismo orden de adquisición de bloqueos en todos los métodos.
Estos tres problemas de concurrencia a menudo se confunden, pero sus mecanismos y consecuencias son fundamentalmente diferentes. Deadlock — detención completa, Starvation — espera infinita de un recurso, Livelock — inactividad activa. Comprender las diferencias es críticamente importante para elegir la estrategia de resolución correcta.
| Característica | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Estado de hilos | Bloqueados (BLOCKED) | Listos (RUNNABLE) | Activos (RUNNABLE) |
| Trabajo realizado | No | No | Sí, pero inútil |
| Causa | Espera circular | Planificación injusta | Manejo incorrecto de conflictos |
| Detección | Thread Dump, timeouts | Monitoreo de progreso | Contador de reintentos |
Starvation (inanición) ocurre cuando el planificador pospone constantemente la ejecución de un hilo de baja prioridad en favor de otros. A diferencia de Deadlock, el hilo no está bloqueado — está listo para ejecutarse pero no recibe tiempo de CPU. En Android, un escenario típico es un hilo en segundo plano de baja prioridad que nunca se ejecuta si el hilo de UI y los hilos de Service están constantemente activos.
Livelock es una situación donde los hilos no están bloqueados pero reaccionan infinitamente a las acciones del otro sin realizar trabajo útil. La analogía clásica son dos personas que se encuentran en un pasillo y ambos intentan ceder el paso, moviéndose en la misma dirección. A diferencia de Deadlock, los hilos en Livelock consumen CPU, agotando la batería del dispositivo.
Thread Dump es la herramienta principal para detectar bloqueos mutuos en JVM y Android Runtime. Durante un volcado, la JVM analiza automáticamente el gráfico de dependencias entre monitores y marca los ciclos de Deadlock. En Android Studio, los volcados de hilos se pueden obtener a través de Android Profiler o el comando kill -3 PID desde ADB Shell.
La detección automática de Deadlock en tiempo de ejecución se implementa mediante temporizadores Watchdog. Si un hilo no completa una operación dentro de un tiempo de espera determinado, el watchdog inicia un volcado y envía un informe al sistema de Crash Reporting (Firebase Crashlytics, Sentry). Según Sentry (Issue Resolution Report, 2024), configurar un watchdog reduce el tiempo de diagnóstico de Deadlock de semanas a unas pocas horas.
Durante el desarrollo, son efectivos el analizador estático ThreadSafe de JetBrains y Checker Framework con el módulo Lock Checker. Estas herramientas analizan el orden de adquisición de bloqueos a nivel de código fuente y advierten sobre posibles ciclos. Además, se recomienda Test-Driven Deadlock Detection — pruebas de estrés que ejecutan operaciones con diferentes órdenes de bloqueo en cientos de hilos.
Mención especial merece la Cooperative Deadlock Detection — un método donde los hilos intercambian información sobre los bloqueos adquiridos a través de un registro global. Si un hilo detecta un ciclo potencial, libera todos los recursos y reintenta la operación. Este enfoque se utiliza en sistemas distribuidos (Apache ZooKeeper, Google Chubby) y se está adoptando gradualmente en el desarrollo móvil a través de bibliotecas como Jetpack Sync.
La forma más confiable es establecer un orden global de adquisición de bloqueos en toda la aplicación. Si todos los hilos siempre adquieren primero el bloqueo con el número más bajo y luego el de número más alto, la espera circular (condición Circular Wait) es imposible. En proyectos grandes, el orden se documenta y se verifica mediante code review.
TryLock es un método de bloqueo que no bloquea un hilo indefinidamente, sino que devuelve false si el bloqueo no se adquiere dentro de un tiempo determinado. En Java, esto se implementa mediante ReentrantLock.tryLock(timeout, TimeUnit), en Kotlin Coroutines — mediante Mutex.withLock con tiempo de espera. En caso de fallo, el hilo libera todos los recursos adquiridos y lo reintenta más tarde.
Algoritmo del banquero es un método teórico de prevención de Deadlock propuesto por Edsger Dijkstra. Modela la asignación de recursos como transacciones bancarias: el sistema no asigna un recurso si esto podría llevar a un estado inseguro (deadlock). En la práctica, el algoritmo rara vez se usa en el desarrollo móvil debido a la dificultad de conocer las necesidades máximas de los hilos de antemano, pero sus principios se utilizan en bases de datos SQLite y sistemas de archivos.
Preguntas frecuentes
No, el bloqueo mutuo requiere al menos dos hilos. En código monohilo, todas las operaciones se ejecutan secuencialmente, por lo que la espera circular es imposible. Sin embargo, Deadlock puede ocurrir entre procesos cuando se usan bloqueos de archivos o semáforos entre procesos.
En corutinas, Deadlock ocurre a nivel de funciones suspendidas (suspend) y no bloquea el hilo del SO, lo que lo hace menos perceptible. Mutex de kotlinx.coroutines es un bloqueo suspendible (suspending) — no bloquea el hilo, pero la corutina no se ejecuta. Para la detección, use DebugProbes del módulo kotlinx-coroutines-debug.
Deadlock en SQLite ocurre cuando dos conexiones de base de datos intentan ejecutar transacciones en diferentes órdenes. SQLite detecta estas situaciones y devuelve el código de error SQLITE_BUSY o SQLITE_LOCKED. En Android, se recomienda usar Room con una única instancia de base de datos y transacciones mediante @Transaction, lo que elimina el Deadlock entre conexiones.
Android Runtime tiene un detector de Deadlock incorporado que se ejecuta cuando se genera un ANR (Application Not Responding). El sistema analiza el Thread Dump de todos los hilos de la aplicación y marca los bloqueos mutuos. El resultado está disponible en /data/anr/traces.txt y en Google Play Console en la sección ANR Reports.
Primero, obtenga un Thread Dump de todos los hilos de la aplicación. Analice qué bloqueos retiene cada hilo y cuáles intenta adquirir. Implemente un temporizador Watchdog con volcado automático cuando se supere el límite de tiempo. Después de la corrección, agregue la regla lint ThreadSafety a su pipeline de CI para prevenir recurrencias.
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