Sincronización Avanzada en Java con CyclicBarrier

La clase java.util.concurrent.CyclicBarrier es una herramienta de sincronización robusta diseñada para escenarios donde un conjunto fijo de hilos debe esperar a que todos lleguen a un punto común antes de continuar con la ejecución. A diferencia de otras utilidades de sincronización, su naturaleza "cíclica" permite que la barrera se reinicie automáticamente después de que los hilos en espera sean liberados.

Conceptos Fundamentales

El funcionamiento de un CyclicBarrier se asemeja a un punto de control en una carrera: ningún corredor puede iniciar la siguiente etapa hasta que todos los miembros de su equipo hayan llegado al punto de reunión. Una vez que el último hilo invoca el método de espera, la barrera se abre y todos los hilos proceden simultáneamente.

Diferencia clave con CountDownLatch:

  • CountDownLatch: Se utiliza para que un hilo principal espere a que N operaciones finalicen. No es reutilizable.
  • CyclicBarrier: Se utiliza para que los hilos de trabajo se esperen mutuamente. Es reutilizable tras cada liberación.

Constructores Principales

Existen dos formas de instanciar esta barrera:

// Define el número de hilos que deben encontrarse en la barrera.
public CyclicBarrier(int parties)

// Además del número de hilos, define una tarea (Runnable) que se ejecutará 
// por el último hilo en llegar, justo antes de liberar a los demás.
public CyclicBarrier(int parties, Runnable barrierAction)

Métodos Esenciales de la API

  • await(): Bloquea el hilo actual hasta que todos los participantes hayan invocado este método en la barrera.
  • await(long timeout, TimeUnit unit): Versión con tiempo de espera limitado. Si se excede el tiempo, lanza una TimeoutException.
  • reset(): Fuerza el reinicio de la barrera a su estado inicial. Si hay hilos esperando, estos recibirán una BrokenBarrierException.
  • isBroken(): Indica si la barrera se encuentra en estado "roto" debido a interrupcinoes o tiempos de espera agotados.

Implementación Práctica: Punto de Sincronización

A continuación, se presenta un ejemplo donde tres operarios deben preparar sus herramientas antes de comenzar una fase de ansamblaje conjunta.

import java.util.concurrent.CyclicBarrier;

public class GestionProcesos {
    public static void main(String[] args) {
        final int NUMERO_TRABAJADORES = 3;
        
        CyclicBarrier puntoReunion = new CyclicBarrier(NUMERO_TRABAJADORES, () -> {
            System.out.println(">> Todos listos. Iniciando fase de ensamblaje...");
        });

        for (int i = 1; i <= NUMERO_TRABAJADORES; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    System.out.println("Operario " + id + " preparando herramientas.");
                    Thread.sleep((long) (Math.random() * 2000)); // Simulando trabajo
                    
                    System.out.println("Operario " + id + " ha llegado a la barrera.");
                    puntoReunion.await();
                    
                    System.out.println("Operario " + id + " comenzando tarea post-barrera.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Funcionamiento Interno

Internamente, CyclicBarrier no utiliza directamente AQS (AbstractQueuedSynchronizer) de la misma forma que un Semaphore. En su lugar, emplea:

  1. ReentrantLock: Para garantizar la exclusión mutua al actualizar el estado de la barrera.
  2. Condition: Para gestionar la cola de hilos que están esperando a que la barrera se complete.
  3. Generations (Generaciones): Un objeto interno que representa el ciclo de vida actual de la barrera. Cada vez que la barrera se abre o se reinicia, se crea una nueva "generación". Si un hilo es interrumpido, la generación actual se marca como "rota" y todos los hilos en espera son notificados.

Comparativa Técnica

Característica CountDownLatch CyclicBarrier
Reutilización No (Un solo uso) Sí (Cíclico)
Acción de Barrera No soportada Soportada vía Runnable
Mecanismo de Error Independiente por hilo Falla en cascada (BrokenBarrier)
Enfoque Sucesos/Eventos Algoritmos paralelos

Escenarios de Uso Recomendados

1. Cálculos por Etapas: Cuando un algoritmo requiere dividir una tarea en partes procesadas en paralelo, pero la etapa N+1 depende de que todos los resultados de la etapa N estén listos.

2. Simulaciones Científicas: Modelado de partículas o sistemas donde cada paso de tiempo debe sincronizarse globalmente.

3. Pruebas de Carga: Sincronizar múltiples hilos de clientes para que inicien una petición HTTP exactamente al mismo tiempo.

Consideraciones y Riesgos

  • Bloqueo Infinito (Deadlock): Si el número de hilos que llaman a await() es menor que el número de parties configurado, los hilos se bloquearán indefinidamente.
  • Manejo de Excepciones: Si un hilo es interrumpido mientras espera, la barrera se rompe. Es vital capturar BrokenBarrierException para decidir si se debe llamar a reset() o abortar la operación.
  • Hilos de la Acción Final: La tarea Runnable definida en el constructor se ejecuta en el contexto del último hilo que llega. Si esta tarea es muy pesada, retrasará la liberación de todos los demás hilos.

Ejemplo de Procesamiento Multietapa

Este ejemplo demuestra cómo la barrera se reutiliza automáticamente para guiar a los hilos a través de distintas fases de procesamiento.

public class ProcesadorDatos {
    public static void main(String[] args) {
        int hilosActivos = 2;
        CyclicBarrier barreraEtapas = new CyclicBarrier(hilosActivos, () -> {
            System.out.println("--- Fin de etapa actual ---");
        });

        Runnable tarea = () -> {
            try {
                // Etapa 1: Carga
                System.out.println(Thread.currentThread().getName() + " cargando datos...");
                barreraEtapas.await();

                // Etapa 2: Transformación
                System.out.println(Thread.currentThread().getName() + " transformando datos...");
                barreraEtapas.await();

                // Etapa 3: Guardado
                System.out.println(Thread.currentThread().getName() + " guardando resultados...");
            } catch (Exception e) {
                Thread.currentThread().interrupt();
            }
        };

        new Thread(tarea, "Procesador-A").start();
        new Thread(tarea, "Procesador-B").start();
    }
}

Etiquetas: java Concurrency CyclicBarrier Multithreading

Publicado el 11-4 09:06