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 unaTimeoutException.reset(): Fuerza el reinicio de la barrera a su estado inicial. Si hay hilos esperando, estos recibirán unaBrokenBarrierException.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:
- ReentrantLock: Para garantizar la exclusión mutua al actualizar el estado de la barrera.
- Condition: Para gestionar la cola de hilos que están esperando a que la barrera se complete.
- 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
BrokenBarrierExceptionpara decidir si se debe llamar areset()o abortar la operación. - Hilos de la Acción Final: La tarea
Runnabledefinida 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();
}
}