Countdownlatch

CountDownLatch

1. ¿Qué es CountDownLatch?

CountDownLatch es una clase de java.util.concurrent que permite a uno o más hilos esperar hasta que un conjunto de operaciones (generalmente ejecutadas por otros hilos) se complete. Funciona como un contador decremental que, al llegar a cero, libera los hilos en espera.

Características clave:

  • Contador inicial: Se define en el constructor (no se puede modificar después).
  • Una vez usado, no se reinicia: A diferencia de CyclicBarrier, no es reutilizable.
  • Thread-safe: Su implementación interna maneja la sincronización.

2. Casos de Uso Comunes

  • Esperar a la inicialización de recursos: Un hilo principal espera a que N hilos terminen de inicializar componentes.
  • Inicio de procesamiento en paralelo: Coordinar el inicio simultáneo de tareas después de una fase de preparación.
  • Esperar la finalización de servicios: Por ejemplo, esperar a que todos los microservicios respondan antes de continuar.

3. Métodos Principales

Constructor

CountDownLatch latch = new CountDownLatch(int count);
  • count: Número de eventos que deben ocurrir antes de que se liberen los hilos en espera.

await()

latch.await();
// Opcional: especificar un tiempo máximo de espera
latch.await(10, TimeUnit.SECONDS);
  • Bloquea el hilo actual hasta que el contador llegue a cero.
  • Importante: Manejar InterruptedException si el hilo es interrumpido durante la espera.

countDown()

latch.countDown();
  • Decrementa el contador en 1. Si el contador llega a cero, libera todos los hilos en espera.

getCount()

int remaining = latch.getCount();
  • Devuelve el valor actual del contador (útil para depuración).

4. Ejemplo Práctico

Escenario: Un hilo principal espera a que 3 trabajadores completen sus tareas.

import java.util.concurrent.CountDownLatch;

public class Example {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 3; i++) {
            executor.submit(() -> {
                try {
                    System.out.println("Tarea iniciada por " + Thread.currentThread().getName());
                    Thread.sleep(1000); // Simular trabajo
                    latch.countDown();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        latch.await(); // Espera a que los 3 hilos terminen
        System.out.println("Todas las tareas han finalizado");
        executor.shutdown();
    }
}

Explicación:

  1. Se crea un CountDownLatch con contador 3.
  2. Tres hilos ejecutan tareas y llaman a countDown() al finalizar.
  3. El hilo principal (main) espera con await() hasta que el contador sea cero.

5. Buenas Prácticas

a. Configuración Correcta del Contador

  • Asegúrate de que el contador inicial coincida con el número de eventos countDown() necesarios.

  • Error común:

    // Incorrecto: Si hay 5 tareas pero el contador es 3
    CountDownLatch latch = new CountDownLatch(3);

b. Usa Timeouts en await()

Evita bloqueos indefinidos:

if (latch.await(5, TimeUnit.SECONDS)) {
    // Contador llegó a cero
} else {
    // Timeout: Manejar fallo
}

c. No Reutilices el Latch

Si necesitas reusabilidad, usa CyclicBarrier.

d. Combina con ExecutorService

Controla la vida de los hilos eficientemente:

ExecutorService executor = Executors.newFixedThreadPool(N);
// ...
executor.shutdown();

e. Manejo de Excepciones

Asegúrate de llamar a countDown() incluso si hay errores:

try {
    // Lógica de la tarea
} catch (Exception e) {
    // Registrar error
} finally {
    latch.countDown(); // Siempre se ejecuta
}

6. Errores Comunes y Soluciones

Error Solución
Contador inicial mayor al número real de countDown() Verificar que el contador coincida con las llamadas reales.
Olvidar llamar a countDown() Usar bloques try-finally para garantizar la ejecución.
No manejar InterruptedException en await() Propagar la interrupción o reintentar.
Usar CountDownLatch para tareas largas Considerar CompletableFuture o otras alternativas.

7. Alternativas a CountDownLatch

  • CyclicBarrier: Útil cuando los hilos necesitan esperarse mutuamente en un punto común (reutilizable).
  • Phaser: Versión más flexible para sincronización en fases dinámicas.
  • CompletableFuture: Para programación asíncrona y encadenamiento de tareas.

Join vs CountDownLatch

Sólo puedes utilizar Thread.join si estás manejando los hilos tú mismo. La mayoría de la gente opta por no ocuparse directamente de las minucias del manejo de hilos, y en su lugar utiliza un ExecutorService para que lo haga por ellos. Los ExecutorServices no revelan directamente cómo están ejecutando tareas, por lo que tendrías que usar un CountDownLatch: (Suponiendo que no quieras apagar todo el servicio, claro).

ExecutorService service = Executors.newFixedThreadPool(5);
final CountDownLatch latch = new CountDownLatch(5);

for(int x = 0; x < 5; x++) {
    service.submit(new Runnable() {
        public void run() {
            // do something
            latch.countDown();
        }
    });
}

latch.await();

El join solamente se puede usar si un hilo principal maneja a los demás hilos, mientras que countDownLatch sirve para coordinar múltiples hilos independientes antes de continuar con la ejecución. CountDownLatch permite una implementación más elegante y flexible que tener que llamar múltiples veces a join para cada hilo que se vaya a crear.