CodeGym /Cursos /JAVA 25 SELF /Sincronizadores de alto nivel

Sincronizadores de alto nivel

JAVA 25 SELF
Nivel 58, Lección 2
Disponible

1. CountDownLatch: inicio por señal

En el mundo multihilo a menudo necesitamos coordinar el trabajo de un grupo de hilos: que todos empiecen, terminen o pasen al siguiente paso juntos. Por ejemplo:

Imagina una carrera. Los coches están en la línea de salida — unos ya han calentado el motor, otros aún revisan los neumáticos. Pero hasta que el juez no agite la bandera, nadie se mueve. Esa es la tarea de coordinación.

Otro ejemplo: preparas la cena con amigos — alguien corta las verduras, alguien pone el agua a hervir, alguien busca dónde se ha metido la sal. Lo importante es que todos acaben los preparativos antes de empezar a cocinar.

Para estos casos, Java nos ofrece herramientas de sincronización ya hechas — seguras, comprensibles y sin el dolor de wait() y notify(). Una de las más útiles es CountDownLatch. Funciona como un cerrojo con contador: mientras no llegue a cero, la «puerta» está cerrada y nadie avanza. Cuando todos se han marcado, el latch se abre y los hilos avanzan sincronizados.

CountDownLatch

CountDownLatch es una «válvula de un solo uso» que permite que uno o varios hilos esperen a que otros hilos terminen un número determinado de operaciones.

Es como la salida de un maratón: todos los corredores están en la línea esperando el disparo de la pistola de salida. En cuanto el juez dispara (el contador llega a cero), todos echan a correr.

Cómo funciona

CountDownLatch es como un silbato de salida para los hilos. Al crearlo indicas un número — por ejemplo, 3. Es como tres señales que hay que recibir antes de que empiece la carrera.

Los hilos que deben esperar a la salida llaman a await(). Están en la línea y listos para salir disparados, pero mantienen el freno. Otros hilos, al ir preparándose, llaman a countDown() — como si dieran la señal: «¡Estoy listo!».

En cuanto el contador llega a cero — ¡pum! — todos los hilos en espera arrancan a la vez.

Pero recuerda: CountDownLatch es de un solo uso. Una vez que el contador ha llegado a cero, no se puede volver atrás. No es un revólver, sino un petardo: estalla y ya está.

Ejemplo: esperar a que finalicen N tareas

import java.util.concurrent.CountDownLatch;

public class LatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int workers = 3;
        CountDownLatch latch = new CountDownLatch(workers);

        for (int i = 1; i <= workers; i++) {
            int id = i;
            new Thread(() -> {
                System.out.println("Trabajador " + id + " empezó a trabajar");
                try { Thread.sleep(500 + id * 200); } catch (InterruptedException ignored) {}
                System.out.println("Trabajador " + id + " terminó el trabajo");
                latch.countDown(); // disminuimos el contador
            }).start();
        }

        System.out.println("El hilo principal espera a que todos los trabajadores terminen...");
        latch.await(); // esperamos a que todos los trabajadores terminen
        System.out.println("¡Todos los trabajadores han terminado! Continuamos con el trabajo principal.");
    }
}

Salida:

El hilo principal espera a que todos los trabajadores terminen...
Trabajador 1 empezó a trabajar
Trabajador 2 empezó a trabajar
Trabajador 3 empezó a trabajar
Trabajador 1 terminó el trabajo
Trabajador 2 terminó el trabajo
Trabajador 3 terminó el trabajo
¡Todos los trabajadores han terminado! Continuamos con el trabajo principal.

Ejemplo: inicio simultáneo «por señal»

CountDownLatch startSignal = new CountDownLatch(1);

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + " espera la salida");
            startSignal.await(); // esperamos la señal
            System.out.println(Thread.currentThread().getName() + " ¡arranca!");
        } catch (InterruptedException ignored) {}
    }).start();
}

Thread.sleep(1000);
System.out.println("¡Señal de salida!");
startSignal.countDown(); // todos los hilos arrancan a la vez

2. CyclicBarrier: fases múltiples, acciones de barrera

CyclicBarrier: nos vemos junto a la hoguera

CyclicBarrier es el punto de encuentro de los hilos. Cada uno recorre su propio camino, hace lo suyo y luego todos se reúnen en la «barrera» — como alrededor de una hoguera en la montaña. Cuando todos han llegado, la barrera se abre y el grupo avanza unido.

La principal diferencia con CountDownLatch es que esta barrera se puede usar una y otra vez. Tras cada parada conjunta se «recarga», y el equipo puede continuar hacia la siguiente etapa.

Imagina: un grupo de excursionistas recorre una ruta larga. Cada uno va a su ritmo: alguien hace fotos de mariposas, alguien busca Wi‑Fi. Pero en cada collado se encuentran junto a la hoguera, se esperan y deciden a dónde ir después. Eso es CyclicBarrier en acción.

Cómo funciona

Creas una barrera e indicas cuántos participantes deben reunirse, por ejemplo, 4. Cada hilo, al llegar al punto de control, llama a await() y espera a los demás. Cuando los cuatro se han reunido, la barrera «hace clic» y deja pasar a todos.

Incluso puedes definir una «acción de barrera» — un trozo de código que se ejecuta exactamente una vez cuando el grupo se ha reunido. Por ejemplo, encender esa hoguera o registrar en el log: «Etapa completada, seguimos». Para ello se pasa un Runnable al constructor.

Importante: a diferencia del CountDownLatch (de un solo uso), CyclicBarrier es reutilizable. Tras cada «reunión» vuelve a estar listo para la siguiente etapa — como una hoguera de campamento que puedes encender una y otra vez.

Ejemplo: sincronización de fases

import java.util.concurrent.CyclicBarrier;

public class BarrierDemo {
    public static void main(String[] args) {
        int parties = 3;
        CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
            System.out.println("¡Todos han llegado a la barrera! Empezamos una nueva fase.");
        });

        for (int i = 1; i <= parties; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    System.out.println("El hilo " + id + " trabaja en la fase 1");
                    Thread.sleep(300 + id * 200);
                    System.out.println("El hilo " + id + " espera la barrera");
                    barrier.await(); // esperamos a los demás

                    System.out.println("El hilo " + id + " trabaja en la fase 2");
                    Thread.sleep(200 + id * 100);
                    System.out.println("El hilo " + id + " espera la barrera (2)");
                    barrier.await(); // esperamos de nuevo

                    System.out.println("El hilo " + id + " ha terminado");
                } catch (Exception e) {
                    System.out.println("Error: " + e);
                }
            }).start();
        }
    }
}

Salida:

Hilo 1 trabaja en la fase 1
Hilo 2 trabaja en la fase 1
Hilo 3 trabaja en la fase 1
Hilo 1 espera la barrera
Hilo 2 espera la barrera
Hilo 3 espera la barrera
¡Todos han llegado a la barrera! Empezamos una nueva fase.
Hilo 1 trabaja en la fase 2
...

Acción de barrera

Se puede pasar al constructor de CyclicBarrier una acción (Runnable) que se ejecutará una vez cuando todos los hilos hayan llegado a la barrera (por ejemplo, actualizar el estado, escribir un log).

Trampas: ¿y si un hilo falla?

Si uno de los hilos lanza una excepción o no llega a la barrera, los demás esperarán para siempre — o recibirán BrokenBarrierException. La barrera se «rompe» y hay que recrearla.

Así es como se puede reescribir esta sección de un modo más vivo, figurado y coloquial — para que suene como una continuación natural de la línea de la «orquesta»:

3. Phaser: el hábil director de un gran concierto

Phaser es algo así como un «superbarrera». Reúne lo mejor de CountDownLatch y CyclicBarrier, pero es mucho más flexible. Es como una orquesta en la que los músicos pueden entrar y salir entre partes del concierto, y el director aun así se asegura de que cada parte empiece cuando todos están listos.

A diferencia de una barrera normal, Phaser trabaja por etapas — las fases se suceden una tras otra. Algunos tocan solo en la primera parte, otros se incorporan después y otros se van antes — todo esto Phaser lo maneja sin problemas.

Cómo funciona

Primero se crea un Phaser, normalmente con un número de participantes — parties. Cada hilo se registra (register()), ejecuta su parte y al final de la fase llama a arriveAndAwaitAdvance() — informa de que ha terminado y espera a los demás. Cuando todos han llegado a este punto, Phaser cambia a la siguiente fase y el proceso se repite.

Si un participante ya no es necesario, puede «hacer una reverencia» y salir del escenario mediante arriveAndDeregister(). Los nuevos, por el contrario, pueden unirse en plena función — mediante register().

Cuándo Phaser es mejor que Barrier

Phaser es la elección adecuada si tu programa no vive en un solo ritmo, sino en varios:

  • el número de hilos cambia sobre la marcha,
  • hay varias etapas y no todos los participantes deben estar en todas,
  • o simplemente quieres la máxima flexibilidad sin peleas con la sincronización manual.

En esencia, Phaser es un director. No solo mueve la batuta, también se adapta a la composición de la orquesta, al número de partes del concierto e incluso a que alguien llegue tarde o se vaya antes.

Ejemplo: procesamiento por etapas con número dinámico de hilos

import java.util.concurrent.Phaser;

public class PhaserDemo {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(1); // hilo principal

        for (int i = 1; i <= 3; i++) {
            phaser.register(); // registramos participante
            int id = i;
            new Thread(() -> {
                for (int phase = 1; phase <= 2; phase++) {
                    System.out.println("El hilo " + id + " trabaja en la fase " + phase);
                    try { Thread.sleep(200 + id * 100); } catch (InterruptedException ignored) {}
                    phaser.arriveAndAwaitAdvance(); // esperamos a los demás
                }
                System.out.println("El hilo " + id + " ha terminado");
                phaser.arriveAndDeregister(); // salimos del phaser
            }).start();
        }

        // El hilo principal también participa en las fases
        for (int phase = 1; phase <= 2; phase++) {
            phaser.arriveAndAwaitAdvance();
            System.out.println("Hilo principal: fase " + phase + " completada");
        }
        phaser.arriveAndDeregister();
        System.out.println("¡Todas las fases han finalizado!");
    }
}

Características:

  • Se pueden añadir/eliminar participantes en caliente.
  • Se puede conocer el número de la fase actual: phaser.getPhase().
  • Se puede finalizar el phaser: phaser.forceTermination().

4. Exchanger: intercambio de bloques de datos entre hilos

Exchanger<T> es un sincronizador para intercambiar datos entre dos hilos. Cada hilo llama a exchange(data) y, cuando ambos hilos se han encontrado, intercambian sus datos.

Analogía: dos mensajeros se encuentran en un cruce y se intercambian paquetes.

¿Cómo funciona?

  • Un hilo llama a exchange(data1) — espera al segundo.
  • El segundo hilo llama a exchange(data2) y ambos reciben los datos del otro.
  • Si el segundo hilo no llega — el primero espera (se puede establecer un time‑out).

Ejemplo: intercambio de búferes entre producer y consumer

import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        // Producer
        new Thread(() -> {
            String data = "Datos del producer";
            try {
                System.out.println("Producer: envía datos");
                String response = exchanger.exchange(data);
                System.out.println("Producer: recibió respuesta: " + response);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // Consumer
        new Thread(() -> {
            try {
                String received = exchanger.exchange("Respuesta del consumer");
                System.out.println("Consumer: recibió datos: " + received);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Salida:

Producer: envía datos
Consumer: recibió datos: Datos del producer
Producer: recibió respuesta: Respuesta del consumer

Aplicaciones:

  • Intercambio de búferes entre hilos (por ejemplo, uno lee de un archivo y otro escribe a la red).
  • Sincronización de fases entre dos hilos.

5. Práctica: procesamiento en canalización paralela

Tarea: el «tick» del juego (fases)

Supongamos que tenemos varios hilos, cada uno responsable de su parte del mundo del juego (por ejemplo, física, IA, renderizado). Todos deben sincronizarse en cada «tick» (fase) para evitar la desincronización.

Solución: Usar CyclicBarrier o Phaser.

import java.util.concurrent.CyclicBarrier;

public class GameTickDemo {
    public static void main(String[] args) {
        int subsystems = 3;
        CyclicBarrier barrier = new CyclicBarrier(subsystems, () -> {
            System.out.println("Todos los subsistemas han terminado el tick. Empezamos el siguiente.");
        });

        for (int i = 1; i <= subsystems; i++) {
            int id = i;
            new Thread(() -> {
                for (int tick = 1; tick <= 5; tick++) {
                    System.out.println("El subsistema " + id + " trabaja en el tick " + tick);
                    try { Thread.sleep(100 + id * 50); } catch (InterruptedException ignored) {}
                    try {
                        barrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

Tarea: «válvula» para un gran número de hilos de trabajo

Supongamos que tenemos 100 hilos de trabajo que deben arrancar simultáneamente tras la preparación (por ejemplo, una prueba de carga).

Solución: Usar CountDownLatch.

import java.util.concurrent.CountDownLatch;

public class MassStartDemo {
    public static void main(String[] args) throws InterruptedException {
        int workers = 100;
        CountDownLatch ready = new CountDownLatch(workers);
        CountDownLatch start = new CountDownLatch(1);

        for (int i = 0; i < workers; i++) {
            new Thread(() -> {
                System.out.println("Hilo listo para la salida");
                ready.countDown(); // señalamos que estamos listos
                try {
                    start.await(); // esperamos la señal común
                    System.out.println("¡El hilo arranca!");
                } catch (InterruptedException ignored) {}
            }).start();
        }

        ready.await(); // esperamos a que todos los hilos estén listos
        System.out.println("¡Todos listos! ¡SALIDA!");
        start.countDown(); // damos la señal de salida
    }
}

6. Errores típicos al trabajar con sincronizadores

Error n.º 1: usar CountDownLatch como una barrera reutilizable.
CountDownLatch es de un solo uso. Tras alcanzar cero no se puede «recargar». Para fases reutilizables usa CyclicBarrier o Phaser.

Error n.º 2: no manejar las excepciones (InterruptedException, BrokenBarrierException).
Los métodos await() pueden lanzar excepciones — manéjalas siempre; de lo contrario, el hilo puede «quedarse colgado» o terminar con error. Vigila InterruptedException y BrokenBarrierException.

Error n.º 3: uno de los hilos no llegó a la barrera.
Si un hilo «se cae» o no llama a await(), los demás esperarán para siempre (o recibirán BrokenBarrierException). Asegúrate de que todos los participantes lleguen a la barrera.

Error n.º 4: olvidar deregister() en Phaser.
Si un hilo termina su trabajo pero no llama a arriveAndDeregister(), Phaser seguirá esperando a un participante «muerto». Elimina siempre los hilos correctamente de Phaser.

Error n.º 5: usar Exchanger con más de dos hilos.
Exchanger funciona solo para intercambio entre dos hilos. Si hay más, obtendrás un deadlock.

Error n.º 6: mezclar distintos sincronizadores sin entender su funcionamiento.
No conviene usar simultáneamente varias barreras/latches diferentes para el mismo grupo de hilos — puede provocar confusión y bloqueos.

1
Tarea
JAVA 25 SELF, nivel 58, lección 2
Bloqueada
Sinfonía de Hilos: Estreno Teatral 🎭
Sinfonía de Hilos: Estreno Teatral 🎭
1
Tarea
JAVA 25 SELF, nivel 58, lección 2
Bloqueada
Intercambio Secreto: Operación "Mensajero Instantáneo" 🕵️
Intercambio Secreto: Operación "Mensajero Instantáneo" 🕵️
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION