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.
GO TO FULL VERSION