CodeGym /Cursos /JAVA 25 SELF /Sincronizadores de alto nível

Sincronizadores de alto nível

JAVA 25 SELF
Nível 58 , Lição 2
Disponível

1. CountDownLatch: início por sinal

No mundo multithread, muitas vezes é necessário organizar o trabalho coordenado de um grupo de threads — para que todos comecem, terminem ou avancem para a próxima etapa juntos. Por exemplo:

Imagine uma corrida. Os carros estão no grid de largada — alguns já aqueceram o motor, outros ainda estão conferindo os pneus. Mas enquanto o juiz não balançar a bandeira, ninguém sai. Essa é justamente a tarefa de coordenação.

Ou outro exemplo: você prepara o jantar com amigos — alguém corta os legumes, alguém coloca água para ferver, alguém procura onde foi parar o sal. O principal é que todos terminem os preparativos antes de começar a cozinhar.

Para esses casos, o Java nos fornece ferramentas de sincronização prontas — seguras, claras e sem a dor de wait() e notify(). Uma das mais úteis é o CountDownLatch. Ele funciona como um contador-trava: enquanto não chega a zero, a “porta” fica fechada e ninguém avança. Quando todos se manifestam — o latch se abre e as threads avançam em sincronia.

CountDownLatch

CountDownLatch é uma “válvula descartável” que permite que uma ou várias threads esperem até que outras threads concluam uma determinada quantidade de operações.

É como a largada de uma maratona: todos os corredores ficam na linha, aguardando o tiro de largada. Assim que o juiz dispara (o countdown chega a zero) — todos saem correndo.

Como isso funciona

CountDownLatch é como um apito de largada para threads. Ao criar, você define um número — por exemplo, 3. É como três sinais que precisam ser recebidos antes do início da corrida.

As threads que devem aguardar o início chamam await(). Elas ficam na linha, prontas para disparar, mas ainda com o freio puxado. Outras threads, realizando a preparação, à medida que ficam prontas chamam countDown() — como se dessem o sinal: “Estou pronto!”.

Quando o contador chega a zero — pum! — todas as threads que aguardavam começam simultaneamente.

Mas lembre-se: CountDownLatch é de uso único. Depois que o contador chega a zero, não dá para voltar atrás. Não é um revólver, é uma bombinha: estourou — e pronto.

Exemplo: aguardando a conclusão de N tarefas

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("Trabalhador " + id + " começou a trabalhar");
                try { Thread.sleep(500 + id * 200); } catch (InterruptedException ignored) {}
                System.out.println("Trabalhador " + id + " terminou o trabalho");
                latch.countDown(); // decrementamos o contador
            }).start();
        }

        System.out.println("A thread principal aguarda a conclusão de todos os trabalhadores...");
        latch.await(); // esperamos até que todos os trabalhadores terminem
        System.out.println("Todos os trabalhadores terminaram! Continuamos o trabalho principal.");
    }
}

Saída:

A thread principal aguarda a conclusão de todos os trabalhadores...
Trabalhador 1 começou a trabalhar
Trabalhador 2 começou a trabalhar
Trabalhador 3 começou a trabalhar
Trabalhador 1 terminou o trabalho
Trabalhador 2 terminou o trabalho
Trabalhador 3 terminou o trabalho
Todos os trabalhadores terminaram! Continuamos o trabalho principal.

Exemplo: início simultâneo “por sinal”

CountDownLatch startSignal = new CountDownLatch(1);

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + " aguarda o início");
            startSignal.await(); // aguardamos o sinal
            System.out.println(Thread.currentThread().getName() + " inicia!");
        } catch (InterruptedException ignored) {}
    }).start();
}

Thread.sleep(1000);
System.out.println("Sinal para iniciar!");
startSignal.countDown(); // todas as threads iniciam simultaneamente

2. CyclicBarrier: fases múltiplas, ações de barreira

CyclicBarrier: vamos nos encontrar na fogueira

CyclicBarrier é o ponto de encontro das threads. Cada uma segue seu próprio percurso, faz algo por conta, e depois todas se reúnem na “barreira” — como em uma fogueira nas montanhas. Quando todas chegam, a barreira se abre e o grupo segue junto.

A principal diferença em relação ao CountDownLatch é que essa barreira pode ser usada repetidamente. Após cada parada conjunta, ela se “recarrega” e a equipe pode continuar rumo à próxima etapa.

Imagine: Um grupo de trilheiros percorre uma longa rota. Cada um segue no seu ritmo: alguém fotografa borboletas, alguém procura Wi‑Fi. Mas em cada passo de montanha eles se encontram na fogueira, esperam uns pelos outros e decidem para onde ir adiante. Isso é o CyclicBarrier em ação.

Como funciona

Você cria uma barreira e informa quantos participantes devem se reunir, por exemplo, 4. Cada thread, ao chegar ao ponto de controle, chama await() — e espera as demais. Quando todas as quatro chegam, a barreira “estala” e libera todas para seguir em frente.

Você pode até definir uma “ação de barreira” — um trecho de código que será executado exatamente uma vez quando o grupo se reunir. Por exemplo, acender a tal fogueira ou registrar um log: “Etapa concluída, vamos em frente”. Para isso, passe um Runnable ao construtor.

Importante: diferente do CountDownLatch de uso único, o CyclicBarrier é reutilizável. Após cada “encontro”, ele fica pronto novamente para a próxima etapa — como uma fogueira de acampamento eterna que pode ser acesa de novo e de novo.

Exemplo: sincronização 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 chegaram à barreira! Iniciando uma nova fase.");
        });

        for (int i = 1; i <= parties; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    System.out.println("Thread " + id + " trabalha na fase 1");
                    Thread.sleep(300 + id * 200);
                    System.out.println("Thread " + id + " aguarda a barreira");
                    barrier.await(); // esperamos os demais

                    System.out.println("Thread " + id + " trabalha na fase 2");
                    Thread.sleep(200 + id * 100);
                    System.out.println("Thread " + id + " aguarda a barreira (2)");
                    barrier.await(); // esperamos novamente

                    System.out.println("Thread " + id + " concluiu o trabalho");
                } catch (Exception e) {
                    System.out.println("Erro: " + e);
                }
            }).start();
        }
    }
}

Saída:

Thread 1 trabalha na fase 1
Thread 2 trabalha na fase 1
Thread 3 trabalha na fase 1
Thread 1 aguarda a barreira
Thread 2 aguarda a barreira
Thread 3 aguarda a barreira
Todos chegaram à barreira! Iniciando uma nova fase.
Thread 1 trabalha na fase 2
...

Ação de barreira

É possível passar no construtor do CyclicBarrier uma ação (Runnable) que será executada uma vez quando todas as threads chegarem à barreira (por exemplo, atualizar estado, imprimir log).

Armadilhas: e se uma thread falhar?

Se uma das threads lançar uma exceção ou não chegar à barreira, as demais ficarão esperando para sempre — ou receberão BrokenBarrierException. A barreira “quebra”, e é preciso recriá-la.

Veja como esta seção pode ser reescrita de forma mais vívida, figurativa e coloquial — para soar como uma continuação natural da linha da “orquestra”:

3. Phaser: um maestro habilidoso de um grande concerto

Phaser é algo como um “superbarreira”. Reúne as melhores características de CountDownLatch e CyclicBarrier, mas é muito mais flexível. É como uma orquestra em que músicos podem entrar e sair entre as partes do concerto, e o maestro ainda assim garante que cada parte comece quando todos estiverem prontos.

Ao contrário de uma barreira comum, o Phaser trabalha por etapas — as fases se sucedem uma após a outra. Alguns tocam apenas na primeira parte, outros entram depois, e outros saem mais cedo — o Phaser lida com tudo isso sem problemas.

Como isso funciona

Primeiro cria-se um Phaser, geralmente com uma quantidade de participantes — parties. Cada thread se registra (register()), executa sua parte e, no fim da fase, chama arriveAndAwaitAdvance() — informa que terminou e espera os demais. Quando todos chegam a esse ponto, o Phaser avança para a próxima fase e o processo se repete.

Se um participante não for mais necessário — ele pode fazer uma “reverência” e sair de cena por meio de arriveAndDeregister(). Novos, ao contrário, podem se juntar durante o concerto — via register().

Quando o Phaser é melhor que o Barrier

Phaser vale a pena quando seu programa vive em mais de um ritmo:

  • o número de threads muda em tempo de execução,
  • há várias etapas, e nem todos os participantes precisam participar de todas,
  • ou você quer a máxima flexibilidade sem complicações com sincronização manual.

No fundo, o Phaser é o maestro. Ele não só marca o compasso, como também se adapta à formação da orquestra, ao número de partes do concerto e até ao fato de alguém se atrasar ou sair mais cedo.

Exemplo: processamento por etapas com número dinâmico de threads

import java.util.concurrent.Phaser;

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

        for (int i = 1; i <= 3; i++) {
            phaser.register(); // registramos o participante
            int id = i;
            new Thread(() -> {
                for (int phase = 1; phase <= 2; phase++) {
                    System.out.println("Thread " + id + " trabalha na fase " + phase);
                    try { Thread.sleep(200 + id * 100); } catch (InterruptedException ignored) {}
                    phaser.arriveAndAwaitAdvance(); // aguardamos os demais
                }
                System.out.println("Thread " + id + " concluiu o trabalho");
                phaser.arriveAndDeregister(); // saímos do phaser
            }).start();
        }

        // A thread principal também participa das fases
        for (int phase = 1; phase <= 2; phase++) {
            phaser.arriveAndAwaitAdvance();
            System.out.println("Thread principal: fase " + phase + " concluída");
        }
        phaser.arriveAndDeregister();
        System.out.println("Todas as fases foram concluídas!");
    }
}

Particularidades:

  • É possível adicionar/remover participantes em tempo de execução.
  • É possível obter o número da fase atual: phaser.getPhase().
  • É possível encerrar o phaser: phaser.forceTermination().

4. Exchanger: troca de blocos de dados entre threads

Exchanger<T> é um sincronizador para troca de dados entre duas threads. Cada thread chama exchange(data), e quando ambas se encontram, elas trocam seus dados.

Analogia: Dois entregadores se encontram no cruzamento e trocam os pacotes.

Como funciona?

  • Uma thread chama exchange(data1) — aguarda a segunda.
  • A segunda thread chama exchange(data2) — ambas recebem os dados uma da outra.
  • Se a segunda thread não aparecer — a primeira espera (é possível definir um timeout).

Exemplo: troca de buffers entre producer e 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 = "Dados do producer";
            try {
                System.out.println("Producer: enviando dados");
                String response = exchanger.exchange(data);
                System.out.println("Producer: recebeu resposta: " + response);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // Consumer
        new Thread(() -> {
            try {
                String received = exchanger.exchange("Resposta do consumer");
                System.out.println("Consumer: recebeu dados: " + received);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Saída:

Producer: enviando dados
Consumer: recebeu dados: Dados do producer
Producer: recebeu resposta: Resposta do consumer

Aplicações:

  • Troca de buffers entre threads (por exemplo, uma lê de um arquivo e outra escreve na rede).
  • Sincronização de fases entre duas threads.

5. Prática: processamento em pipeline paralelo

Tarefa: “tick” de jogo (fases)

Suponha que temos várias threads, cada uma responsável por uma parte do mundo do jogo (por exemplo, física, IA, renderização). Todas devem se sincronizar a cada “tick” (fase), para não haver dessincronização.

Solução: Use CyclicBarrier ou 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 os subsistemas concluíram o tick. Iniciando o próximo.");
        });

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

Tarefa: “Válvula” para um grande número de workers

Suponha que temos 100 threads workers, que devem iniciar simultaneamente após a preparação (por exemplo, um teste de carga).

Solução: Use 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("Thread pronta para iniciar");
                ready.countDown(); // sinalizamos que está pronta
                try {
                    start.await(); // aguardamos o sinal geral
                    System.out.println("Thread inicia!");
                } catch (InterruptedException ignored) {}
            }).start();
        }

        ready.await(); // aguardamos até que todas as threads estejam prontas
        System.out.println("Todos prontos! INICIAR!");
        start.countDown(); // damos o sinal para iniciar
    }
}

6. Erros típicos ao trabalhar com sincronizadores

Erro nº 1: Usar CountDownLatch como barreira reutilizável.
CountDownLatch é de uso único! Após atingir zero, ele não pode ser “recarregado”. Para fases reutilizáveis, use CyclicBarrier ou Phaser.

Erro nº 2: Não tratar exceções (InterruptedException, BrokenBarrierException).
Os métodos await() podem lançar exceções — sempre trate-as, caso contrário a thread pode “travar” ou terminar com erro. Atente para InterruptedException e BrokenBarrierException.

Erro nº 3: Uma das threads não chegou à barreira.
Se uma thread “cair” ou não chamar await(), as demais vão esperar para sempre (ou receber BrokenBarrierException). Garanta que todos os participantes alcancem a barreira.

Erro nº 4: Esquecer o deregister() no Phaser.
Se a thread terminar o trabalho, mas não chamar arriveAndDeregister(), o Phaser vai esperar por um participante “morto”. Sempre remova as threads corretamente do Phaser.

Erro nº 5: Usar Exchanger para mais de duas threads.
Exchanger funciona apenas para troca entre duas threads. Se houver mais threads — haverá deadlock.

Erro nº 6: Misturar diferentes sincronizadores sem entender seu funcionamento.
Não use simultaneamente várias barreiras/latches para o mesmo grupo de threads — isso pode levar a confusão e travamentos.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION