CodeGym /Cursos /JAVA 25 SELF /Livelock y Starvation: definición y ejemplos

Livelock y Starvation: definición y ejemplos

JAVA 25 SELF
Nivel 53 , Lección 1
Disponible

1. Introducción a Livelock

Si deadlock es cuando los hilos se quedan parados esperándose mutuamente para siempre, entonces livelock (bloqueo vivo) es cuando los hilos, aunque están activos, hacen cosas constantemente, se ceden el paso unos a otros, pero... ¡nadie avanza! Imagina a dos personas corteses en un pasillo estrecho: «¡Uy, pase usted!» — «No, usted» — «No, usted» — y así hasta el infinito.

Definición formal

Livelock es una situación en la que los hilos no están bloqueados, pero, debido a cambios constantes de su estado en respuesta a las acciones de otros hilos, no pueden finalizar su trabajo. Están «vivos», reaccionan activamente, pero no realizan trabajo útil.

¿Cómo se ve en la práctica?

  • Los hilos no quedan bloqueados para siempre, pero se atascan en un bucle infinito de cesiones.
  • El sistema no se cuelga, pero tampoco hace lo que debe.

Ejemplo de la vida real

  • Dos robots que deben cruzarse en un pasillo estrecho y, cada vez, ambos dan un paso hacia el mismo lado a la vez — y vuelven a estorbarse.
  • Dos hilos que, cada vez que detectan que el recurso está ocupado, se ceden mutuamente... infinitamente.

2. Ejemplo de livelock en Java

Simulemos un livelock en código. Para simplificar, tomemos a dos «trabajadores» que necesitan una sola cuchara. A diferencia del deadlock, si la cuchara está ocupada, ceden cortésmente e intentan de nuevo, pero a la vez, de forma síncrona.

Ejemplo de código: «Trabajadores corteses»

public class LivelockDemo {
    static class Spoon {
        private Worker owner;

        public Spoon(Worker owner) {
            this.owner = owner;
        }

        public Worker getOwner() {
            return owner;
        }

        public synchronized void setOwner(Worker owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            // Uso de la cuchara (no hace nada)
        }
    }

    static class Worker {
        private final String name;
        private boolean isHungry = true;

        public Worker(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public boolean isHungry() {
            return isHungry;
        }

        public void eatWith(Spoon spoon, Worker other) {
            while (isHungry) {
                // Si la cuchara no está conmigo — espero
                if (spoon.getOwner() != this) {
                    try {
                        Thread.sleep(1); // Esperamos a que la cuchara quede libre
                    } catch (InterruptedException ignored) {}
                    continue;
                }
                // Si el otro tiene hambre — cedo la cuchara
                if (other.isHungry()) {
                    System.out.println(name + ": Cedo la cuchara a " + other.getName());
                    spoon.setOwner(other);
                    continue;
                }
                // ¡Como!
                System.out.println(name + ": ¡Estoy comiendo!");
                spoon.use();
                isHungry = false;
                System.out.println(name + ": ¡Ya he comido!");
                spoon.setOwner(other);
            }
        }
    }

    public static void main(String[] args) {
        final Worker alice = new Worker("Alicia");
        final Worker bob = new Worker("Bob");
        final Spoon spoon = new Spoon(alice);

        Thread t1 = new Thread(() -> alice.eatWith(spoon, bob));
        Thread t2 = new Thread(() -> bob.eatWith(spoon, alice));

        t1.start();
        t2.start();
    }
}

¿Qué ocurre?

  • Alicia y Bob están hambrientos; la cuchara está primero con Alicia.
  • Alicia ve que Bob también tiene hambre y le cede la cuchara.
  • Ahora la cuchara está con Bob, pero él ve que Alicia tiene hambre y se la cede.
  • La cuchara «salta» entre los trabajadores y nadie come: no hay progreso.

¿Cómo se ve en la salida?

Alicia: Cedo la cuchara a Bob
Bob: Cedo la cuchara a Alicia
Alicia: Cedo la cuchara a Bob
Bob: Cedo la cuchara a Alicia
...

¿Cómo eliminar un livelock?

Se puede eliminar un livelock si reducimos un poco la «cortesía» de los hilos. Ayuda añadir una pausa aleatoria antes de reintentar (por ejemplo, mediante Thread.sleep) — así los hilos dejan de reaccionar de forma síncrona. También funciona una estrategia más «decidida»: si ya has cedido, espera más antes de volver a intentarlo. Y no te excedas con la caballerosidad en los algoritmos: las cesiones excesivas también llevan a bloqueos.

3. Starvation (inanición del hilo)

Si livelock es «cortesía eterna», starvation (inanición) es cuando uno o varios hilos no obtienen acceso al recurso o al procesador porque otros siempre se les adelantan.

Definición formal

Starvation es una situación en la que un hilo no puede obtener acceso al recurso necesario (CPU, memoria, bloqueo) porque otros hilos lo adelantan continuamente. Como resultado, el hilo «hambriento» o bien se ejecuta muy rara vez, o no se ejecuta en absoluto.

Causas de starvation

  • Bloqueos injustos. Por ejemplo, un bloque synchronized normal no garantiza que el hilo que más tiempo lleva esperando entre primero.
  • Prioridades de hilos. Si los hilos de alta prioridad ocupan constantemente el procesador, los de baja prioridad pueden «morirse de hambre» (setPriority).
  • Bucles infinitos en otros hilos. Si alguien no cede la CPU (no llama a Thread.sleep o Thread.yield()), otros hilos pueden no obtener tiempo de ejecución.

4. Ejemplo de starvation en Java

Ejemplo: un hilo de baja prioridad no se ejecuta

public class StarvationDemo {
    public static void main(String[] args) {
        Runnable highPriorityTask = () -> {
            while (true) {
                // Trabajo intensivo, no cede la CPU
            }
        };

        Runnable lowPriorityTask = () -> {
            while (true) {
                System.out.println("¡Soy un hilo de baja prioridad!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {}
            }
        };

        Thread high1 = new Thread(highPriorityTask);
        Thread high2 = new Thread(highPriorityTask);
        Thread low = new Thread(lowPriorityTask);

        high1.setPriority(Thread.MAX_PRIORITY); // 10
        high2.setPriority(Thread.MAX_PRIORITY); // 10
        low.setPriority(Thread.MIN_PRIORITY);   // 1

        high1.start();
        high2.start();
        low.start();
    }
}

¿Cómo se manifiesta?

  • Los hilos de alta prioridad están ocupados todo el tiempo y no ceden la CPU.
  • El hilo de baja prioridad casi no se ejecuta (o no se ejecuta en absoluto).
  • En JVM/SO modernos, las prioridades pueden ser suavizadas por el planificador, pero en algunos sistemas la inanición es notable.

Otro ejemplo: starvation debido a un bloqueo injusto

public class StarvationLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 5 hilos que capturan el lock todo el tiempo
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        // Mantenemos el lock durante mucho tiempo
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException ignored) {}
                    }
                }
            }).start();
        }

        // Un hilo hambriento
        new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    System.out.println("¡El hilo hambriento obtuvo el lock!");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ignored) {}
                }
            }
        }).start();
    }
}

En este ejemplo, el hilo «hambriento» puede tardar mucho en obtener acceso al lock si otros hilos lo ocupan constantemente.

5. Cómo detectar y prevenir livelock y starvation

¿Cómo detectarlo?

  • Livelock: el programa funciona, los hilos no están colgados, pero no hay progreso (no hay resultado, no se sale de los bucles).
  • Starvation: algunos hilos casi no se ejecutan (mensajes raros en los logs o ausencia de ellos).

Herramientas

  • Registro de logs: marca el inicio/fin del trabajo, la toma/liberación de recursos.
  • Monitorización: VisualVM, Java Mission Control — observa qué hilos están activos y en qué.
  • Thread dump: comprueba si los hilos están atascados esperando un lock.

¿Cómo evitarlo?

Para livelock:

  • No hagas cesiones demasiado «corteses»: añade un pequeño retraso aleatorio antes de reintentar (Thread.sleep).
  • Introduce aleatoriedad en el orden de los reintentos para evitar el comportamiento síncrono de los hilos.
  • Usa estructuras/algoritmos no bloqueantes (variables atómicas, enfoque CAS).

Para starvation:

  • Usa bloqueos «justos». Por ejemplo, ReentrantLock con fairness:
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock(true); // modo justo
  • No abuses de las prioridades de los hilos: suele ser mejor dejar la prioridad por defecto.
  • Minimiza el tiempo dentro de las secciones críticas (synchronized/Lock).
  • Usa colas de tareas donde la atención sea cercana a FIFO.

Tabla: Deadlock, Livelock, Starvation — comparación

Problema Qué ocurre ¿Los hilos están «vivos»? ¿Hay progreso? Síntoma típico
Deadlock Todos se esperan entre sí No No El programa «se quedó colgado»
Livelock Todos ceden, pero no avanzan No Los hilos trabajan, pero no hay resultado
Starvation Unos trabajan, otros casi no Sí (algunos) Parcial Algunos hilos padecen «inanición»

Analogías y datos interesantes

  • Livelock — como dos personas que a la vez dan un paso a la izquierda para apartarse y vuelven a chocarse.
  • Starvation — como una cola en una tienda donde el cajero atiende solo a «los suyos» y el resto espera eternamente.

Dato interesante: el livelock se da con menos frecuencia que el deadlock, pero es más difícil de detectar — ¡el programa no «se cuelga», sino que hace algo!

6. Errores típicos al trabajar con livelock y starvation

Error n.º 1: «Cesiones corteses» sin espera. Si los hilos se ceden con demasiada frecuencia sin pausa, pueden caer en un livelock. Añade un pequeño retraso aleatorio antes de reintentar capturar el recurso (Thread.sleep).

Error n.º 2: Esperar solo con synchronized, sin bloqueos justos. Con muchos hilos, un synchronized normal no garantiza que el «más hambriento» obtenga acceso. Usa ReentrantLock con fairness cuando sea crítico.

Error n.º 3: Abusar de las prioridades de los hilos. Intentar «acelerar» hilos importantes con setPriority a menudo conduce a starvation de otros. No toques las prioridades sin una necesidad real.

Error n.º 4: Falta de monitorización y logging. Livelock y starvation son difíciles de detectar sin logs: el programa «funciona», pero no hay resultado. Registra eventos clave y usa perfiladores/dumps de hilos.

Error n.º 5: Secciones críticas demasiado largas. Si un hilo mantiene el lock durante mucho tiempo, los demás esperarán (o «pasarán hambre»). Minimiza el tiempo dentro de los bloques synchronized/Lock.

1
Tarea
JAVA 25 SELF, nivel 53, lección 1
Bloqueada
Encrucijada de cortesía: Modelando "livelock" 🚶‍♂️🚶‍♀️
Encrucijada de cortesía: Modelando "livelock" 🚶‍♂️🚶‍♀️
1
Tarea
JAVA 25 SELF, nivel 53, lección 1
Bloqueada
Pobre informe en segundo plano: ejemplo de "starvation" debido a las prioridades 📉
Pobre informe en segundo plano: ejemplo de "starvation" debido a las prioridades 📉
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION