1. CountDownLatch: start on signal
In the multithreaded world you often need to orchestrate a group of threads—so they start, finish, or move to the next stage together. For example:
Imagine a race. Cars are at the starting line—some have warmed up the engine, others are still checking the tires. But until the referee waves the flag, nobody moves. That’s the coordination problem.
Or another example: you’re cooking dinner with friends—someone chops vegetables, someone puts water on, someone is looking for the missing salt. The key is that everyone finishes prep before cooking starts.
For such cases Java gives us ready-made synchronization tools—safe, understandable, and without the pain of wait() and notify(). One of the most useful is CountDownLatch. It works like a counter lock: until it drops to zero, the “door” is closed and no one proceeds. Once everyone has checked in, the latch opens and the threads surge forward in sync.
CountDownLatch
CountDownLatch is a “one-shot valve” that allows one or more threads to wait until other threads complete a specified number of operations.
It’s like the start of a marathon: all runners stand at the line and wait for the starter pistol. As soon as the official fires (the countdown reaches zero), everyone runs.
How it works
CountDownLatch is like a starting whistle for threads. When you create it, you set a number—say, 3. That’s like three signals that must be received before the race begins.
Threads that must wait for the start call await(). They are on the line and ready to burst forward but still hold the brakes. Other threads, doing preparation, call countDown() as they get ready—as if signaling “I’m ready!”
As soon as the counter reaches zero—bam!—all waiting threads start at once.
But remember: CountDownLatch is one-shot. After the counter reaches zero, you can’t reset it. It’s not a revolver; it’s a firecracker: it pops—and that’s it.
Example: waiting for N tasks to complete
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("Worker " + id + " started work");
try { Thread.sleep(500 + id * 200); } catch (InterruptedException ignored) {}
System.out.println("Worker " + id + " finished work");
latch.countDown(); // decrement the counter
}).start();
}
System.out.println("Main thread is waiting for all workers to finish...");
latch.await(); // wait until all workers finish
System.out.println("All workers are done! Continuing the main work.");
}
}
Output:
Main thread is waiting for all workers to finish...
Worker 1 started work
Worker 2 started work
Worker 3 started work
Worker 1 finished work
Worker 2 finished work
Worker 3 finished work
All workers are done! Continuing the main work.
Example: simultaneous start “on signal”
CountDownLatch startSignal = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is waiting to start");
startSignal.await(); // wait for the signal
System.out.println(Thread.currentThread().getName() + " starts!");
} catch (InterruptedException ignored) {}
}).start();
}
Thread.sleep(1000);
System.out.println("Start signal!");
startSignal.countDown(); // all threads start simultaneously
2. CyclicBarrier: multiple phases, barrier actions
CyclicBarrier: meet at the campfire
CyclicBarrier is a meeting point for threads. Each one runs its own route, does its own work, and then they all meet at the “barrier”—like a campfire in the mountains. When everyone arrives, the barrier opens and the group moves on together.
The key difference from CountDownLatch is that you can reuse this barrier over and over. After each joint stop it “recharges,” and the team can continue to the next stage.
Imagine: A group of hikers follows a long route. Each moves at their own pace: someone photographs butterflies, someone looks for Wi‑Fi. But at every pass they meet at the campfire, wait for each other, and decide where to go next. That’s CyclicBarrier in action.
How it works
You create a barrier and specify how many participants must gather, for example 4. Each thread, upon reaching the checkpoint, calls await() and waits for the others. When all four have gathered, the barrier “clicks” and lets everyone proceed.
You can even set a “barrier action”—a piece of code that runs exactly once when the group has gathered. For example, light that campfire or write a log entry: “Stage completed, moving on.” To do this, you pass a Runnable to the constructor.
Important: unlike the one-shot CountDownLatch, CyclicBarrier is reusable. After each “gathering,” it’s ready for the next stage—like an eternal campfire you can light again and again.
Example: phase synchronization
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("Everyone reached the barrier! Starting a new phase.");
});
for (int i = 1; i <= parties; i++) {
int id = i;
new Thread(() -> {
try {
System.out.println("Thread " + id + " working in phase 1");
Thread.sleep(300 + id * 200);
System.out.println("Thread " + id + " is waiting for the barrier");
barrier.await(); // wait for the others
System.out.println("Thread " + id + " working in phase 2");
Thread.sleep(200 + id * 100);
System.out.println("Thread " + id + " is waiting for the barrier (2)");
barrier.await(); // wait again
System.out.println("Thread " + id + " finished work");
} catch (Exception e) {
System.out.println("Error: " + e);
}
}).start();
}
}
}
Output:
Thread 1 working in phase 1
Thread 2 working in phase 1
Thread 3 working in phase 1
Thread 1 is waiting for the barrier
Thread 2 is waiting for the barrier
Thread 3 is waiting for the barrier
Everyone reached the barrier! Starting a new phase.
Thread 1 working in phase 2
...
Barrier action
You can pass a CyclicBarrier action (Runnable) to the constructor that will run once when all threads have reached the barrier (for example, update state or print a log).
Pitfalls: what if one thread crashes?
If one of the threads throws an exception or never reaches the barrier, the others will wait forever—or get a BrokenBarrierException. The barrier “breaks,” and you need to recreate it.
Here is how this section can be rewritten in a livelier, more vivid, conversational style—so it sounds like a natural continuation of the “orchestra” theme:
3. Phaser: a skilled conductor of a grand concert
Phaser is a kind of “super barrier.” It combines the best traits of CountDownLatch and CyclicBarrier, but is much more flexible. It’s like an orchestra where musicians can join and leave between movements, and the conductor still ensures each part starts when everyone is ready.
Unlike a simple barrier, Phaser works in stages—phases follow one after another. Someone plays only in the first movement, someone joins later, and someone leaves earlier—Phaser handles it calmly.
How it works
First you create a Phaser, usually with a given number of participants—parties. Each thread registers (register()), performs its part, and at the end of the phase calls arriveAndAwaitAdvance()—it reports it’s done and waits for the others. When everyone has reached this point, the Phaser advances to the next phase and the process repeats.
If a participant is no longer needed, they can bow and leave the stage via arriveAndDeregister(). New ones, on the contrary, can join right during the concert—via register().
When Phaser is better than Barrier
Phaser is a good choice if your program lives in several rhythms rather than one:
- the number of threads changes on the fly,
- there are multiple stages and not all participants must take part in all of them,
- or you just want maximum flexibility without fussing with manual synchronization.
In essence, Phaser is the conductor. It not only waves the baton, it adapts to the orchestra’s lineup, to the number of movements, and even to someone being late or leaving early.
Example: staged processing with a dynamic number of threads
import java.util.concurrent.Phaser;
public class PhaserDemo {
public static void main(String[] args) {
Phaser phaser = new Phaser(1); // main thread
for (int i = 1; i <= 3; i++) {
phaser.register(); // register a participant
int id = i;
new Thread(() -> {
for (int phase = 1; phase <= 2; phase++) {
System.out.println("Thread " + id + " working in phase " + phase);
try { Thread.sleep(200 + id * 100); } catch (InterruptedException ignored) {}
phaser.arriveAndAwaitAdvance(); // wait for the others
}
System.out.println("Thread " + id + " finished work");
phaser.arriveAndDeregister(); // leave the phaser
}).start();
}
// The main thread also participates in the phases
for (int phase = 1; phase <= 2; phase++) {
phaser.arriveAndAwaitAdvance();
System.out.println("Main thread: phase " + phase + " completed");
}
phaser.arriveAndDeregister();
System.out.println("All phases completed!");
}
}
Notes:
- You can add/remove participants on the fly.
- You can get the current phase number: phaser.getPhase().
- You can terminate the phaser: phaser.forceTermination().
4. Exchanger: exchanging chunks of data between threads
Exchanger<T> is a synchronizer for exchanging data between two threads. Each thread calls exchange(data), and when both threads meet, they swap their data.
Analogy: Two couriers meet at an intersection and exchange packages.
How it works
- One thread calls exchange(data1)—it waits for the second.
- The second thread calls exchange(data2)—both receive each other’s data.
- If the second thread doesn’t arrive, the first waits (you can set a timeout).
Example: exchanging buffers between producer and 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 = "Data from producer";
try {
System.out.println("Producer: sending data");
String response = exchanger.exchange(data);
System.out.println("Producer: received response: " + response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// Consumer
new Thread(() -> {
try {
String received = exchanger.exchange("Response from consumer");
System.out.println("Consumer: received data: " + received);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
Output:
Producer: sending data
Consumer: received data: Data from producer
Producer: received response: Response from consumer
Use cases:
- Exchanging buffers between threads (e.g., one reads from a file, another writes to the network).
- Phase synchronization between two threads.
5. Practice: parallel pipeline processing
Task: game “tick” (phases)
Suppose you have several threads, each responsible for its part of the game world (for example, physics, AI, rendering). They all must synchronize on every “tick” (phase) to avoid desynchronization.
Solution: Use CyclicBarrier or 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("All subsystems finished the tick. Starting the next one.");
});
for (int i = 1; i <= subsystems; i++) {
int id = i;
new Thread(() -> {
for (int tick = 1; tick <= 5; tick++) {
System.out.println("Subsystem " + id + " working in tick " + tick);
try { Thread.sleep(100 + id * 50); } catch (InterruptedException ignored) {}
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
Task: “valve” for a large number of workers
Suppose we have 100 worker threads that must start simultaneously after preparation (for example, a load test).
Solution: 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 is ready to start");
ready.countDown(); // signal readiness
try {
start.await(); // wait for the common signal
System.out.println("Thread starts!");
} catch (InterruptedException ignored) {}
}).start();
}
ready.await(); // wait until all threads are ready
System.out.println("Everyone is ready! START!");
start.countDown(); // give the start signal
}
}
6. Common mistakes when working with synchronizers
Mistake #1: Using CountDownLatch as a reusable barrier.
CountDownLatch is one-shot! After it reaches zero, it cannot be “reloaded.” For reusable phases use CyclicBarrier or Phaser.
Mistake #2: Not handling exceptions (InterruptedException, BrokenBarrierException).
Methods like await() can throw exceptions—always handle them, otherwise a thread may “hang” or terminate with an error. Watch for InterruptedException and BrokenBarrierException.
Mistake #3: One of the threads did not reach the barrier.
If one thread “crashes” or never calls await(), the others will wait forever (or get a BrokenBarrierException). Ensure all participants reach the barrier.
Mistake #4: Forgot deregister() in Phaser.
If a thread finishes but doesn’t call arriveAndDeregister(), the Phaser will wait for a “dead” participant. Always remove threads from the Phaser properly.
Mistake #5: Using Exchanger for more than two threads.
Exchanger works only for exchange between two threads. If there are more threads—you’ll get a deadlock.
Mistake #6: Mixing different synchronizers without understanding how they work.
Do not use several different barriers/latches for the same group of threads at the same time—it can lead to confusion and hangs.
GO TO FULL VERSION