CodeGym /Cursos /JAVA 25 SELF /Deadlock: causas, ejemplos, resolución

Deadlock: causas, ejemplos, resolución

JAVA 25 SELF
Nivel 53, Lección 0
Disponible

1. Profundizando en el deadlock

Deadlock («interbloqueo») — es una situación en la que dos o más hilos se esperan mutuamente para siempre: cada uno mantiene algún recurso e intenta obtener otro que ya está ocupado por un hilo vecino. Como resultado, nadie puede continuar y el programa “se cuelga”. Es como dos coches en un puente estrecho, cara a cara: hasta que uno no dé marcha atrás, nadie pasa.

En Java un deadlock no es una excepción (Exception), sino un “cuelgue” del programa. Los hilos no terminan ni fallan con error, simplemente se esperan entre sí indefinidamente. Por eso estos fallos son traicioneros: solo se manifiestan “en determinadas circunstancias”.

Cuatro condiciones para que se produzca un deadlock

  • 1. Exclusión mutua (Mutual Exclusion)
    Un recurso solo puede ser adquirido por un hilo a la vez (por ejemplo, un objeto monitor, un archivo).
  • 2. Retención y espera (Hold and Wait)
    Un hilo mantiene un recurso e intenta obtener un segundo sin soltar el primero.
  • 3. No expropiación (No Preemption)
    No se puede “arrebatar” un recurso a un hilo: solo el propio hilo puede liberarlo.
  • 4. Espera circular (Circular Wait)
    Existe una cadena cerrada donde cada hilo espera un recurso mantenido por el siguiente en la cadena.

Un deadlock es posible solo si se cumplen TODAS estas condiciones simultáneamente. Basta con violar al menos una — y el interbloqueo es imposible.

2. Ejemplo de código con deadlock

Esquema

  • Hay dos recursos: lock1 y lock2 (objetos normales).
  • El hilo A primero adquiere lock1 y luego intenta obtener lock2.
  • El hilo B primero adquiere lock2 y luego intenta obtener lock1.

Ejemplo (¡no lo hagas en casa!)

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        // Hilo 1
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Hilo 1: ha adquirido lock1");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Hilo 1: intenta adquirir lock2");
                synchronized (lock2) {
                    System.out.println("Hilo 1: ha adquirido lock2");
                }
            }
        });

        // Hilo 2
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Hilo 2: ha adquirido lock2");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Hilo 2: intenta adquirir lock1");
                synchronized (lock1) {
                    System.out.println("Hilo 2: ha adquirido lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

¿Qué ocurrirá al ejecutarlo?

  • El hilo 1 adquiere lock1, el hilo 2 — lock2.
  • Ambos “duermen” 100 ms, tiempo suficiente para adquirir su primer lock.
  • Después, cada uno intenta adquirir el segundo lock, que ya está siendo mantenido por el otro hilo.
  • Ambos hilos se esperan mutuamente indefinidamente — se produce un deadlock.

Verás en la consola:

Hilo 1: ha adquirido lock1
Hilo 2: ha adquirido lock2
Hilo 1: intenta adquirir lock2
Hilo 2: intenta adquirir lock1

¿Por qué se produce un deadlock? Análisis detallado

  • Exclusión mutua: cada lock solo puede ser mantenido por un hilo a la vez.
  • Retención y espera: el hilo 1 mantiene lock1 y espera lock2; el hilo 2 mantiene lock2 y espera lock1.
  • No expropiación: nadie puede “arrancar” el lock desde fuera — solo saliendo del bloque synchronized.
  • Espera circular: la espera se cierra en círculo entre ambos hilos.

3. Cómo evitar y prevenir un deadlock

Adquirir recursos siempre en el mismo orden

Regla principal: si varios hilos deben adquirir múltiples recursos — hazlo siempre en el mismo orden. Por ejemplo: primero lock1, luego lock2 — en todos los lugares del código.

public void doSomething() {
    Object firstLock = lock1;
    Object secondLock = lock2; // orden único "lock1 -> lock2"
    synchronized (firstLock) {
        synchronized (secondLock) {
            // Trabajo con ambos recursos
        }
    }
}

Con un orden único, la espera circular no es posible — se rompe una condición del deadlock.

Usar tryLock con timeout (ReentrantLock)

Cuando no se sabe de antemano qué recursos serán necesarios, usa ReentrantLock y el método tryLock para no esperar indefinidamente.

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockDemo {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public void doWork() {
        try {
            if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            // Sección crítica
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("No se pudo adquirir lock2, revertimos");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("No se pudo adquirir lock1, revertimos");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Ventaja: puedes deshacer la operación e intentarlo más tarde. Desventaja: el código es más complejo, pero el deadlock queda descartado.

Minimiza el tiempo de retención de los locks

Mantén el lock el menor tiempo posible. Dentro de synchronized/lock haz solo lo necesario; saca todo lo demás fuera de la sección crítica.

Evita locks anidados

Cuantos menos synchronized/lock anidados, menor el riesgo de interbloqueo. Si es posible, usa un único lock para recursos relacionados.

Usa estructuras thread-safe ya preparadas

La biblioteca estándar ya resuelve muchos casos: colecciones como ConcurrentHashMap y otras clases de java.util.concurrent minimizan el riesgo de deadlock y simplifican el código.

4. Diagnóstico del deadlock

Thread Dump y jstack

Thread Dump — una instantánea del estado de todos los hilos en la JVM.

Obtenerlo desde la consola:

jstack <pid>

Donde <pid> es el identificador del proceso Java (puedes obtenerlo con jps).

En el IDE suele haber un botón «Thread Dump» en el panel de depuración.

En el volcado, busca:

  • Estados BLOCKED o WAITING.
  • Mensajes del tipo waiting to lock ... y locked ....
  • La frase de la JVM: "Found one Java-level deadlock:" — cuando se detecta el interbloqueo.

Ejemplo de un fragmento del dump:

"Thread-1":
  waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
  which is held by "Thread-1"

Eso es un deadlock: los hilos se están bloqueando mutuamente.

VisualVM y otras herramientas

  • VisualVM — herramienta gratuita para analizar hilos y detectar deadlocks.
  • Java Mission Control / Flight Recorder — monitorización y perfilado avanzados.

En VisualVM puedes ver el árbol de hilos, sus estados y recibir un aviso sobre interbloqueos.

Tabla: «Cómo no caer en un deadlock»

Causa del deadlock Cómo evitarlo
Adquisición de recursos en distinto orden Respetar siempre un mismo orden de adquisición de recursos
synchronized anidados Minimizar la anidación; si es posible, usar un solo lock
Mantener el lock durante mucho tiempo Reducir el trabajo dentro de la sección crítica
Uso simultáneo de varios locks Aplicar tryLock con timeout y disponer de lógica de rollback
Colecciones no thread-safe Usar colecciones concurrentes (ConcurrentHashMap y otras)

5. Errores típicos al trabajar con deadlocks

Error n.º 1: Adquirir recursos en distinto orden. La causa más frecuente — distintos hilos toman los locks en diferente orden. Aunque “parezca más rápido”, respeta un orden único — de lo contrario, la espera circular está garantizada.

Error n.º 2: synchronized anidados sin necesidad. La anidación innecesaria aumenta el riesgo de interbloqueo y complica el código. Simplifica el modelo de locks.

Error n.º 3: Ignorar tryLock y los timeouts. Un bloqueo mediante synchronized hará que el hilo espere indefinidamente. Si no hay certeza, usa tryLock con timeout y lógica de rollback.

Error n.º 4: Ejecutar código pesado dentro del lock. Peticiones de red, I/O y cálculos costosos bajo lock incrementan mucho la probabilidad de problemas. Sácalos fuera de la sección crítica.

Error n.º 5: No analizar el Thread Dump. Thread Dump es tu mejor aliado para encontrar interbloqueos. Usa jstack y analiza los estados BLOCKED/WAITING, las cadenas “holding/waiting to lock”.

1
Tarea
JAVA 25 SELF, nivel 53, lección 0
Bloqueada
Pasar el testigo entre robots: Peligro de interbloqueo 🤖
Pasar el testigo entre robots: Peligro de interbloqueo 🤖
1
Tarea
JAVA 25 SELF, nivel 53, lección 0
Bloqueada
Rescate del interbloqueo: Ingenieros inteligentes con tiempo de espera 🛠️
Rescate del interbloqueo: Ingenieros inteligentes con tiempo de espera 🛠️
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION