The fundamental distinction between CountDownLatch and CyclicBarrier lies in which thread gets blocked. When using CountDownLatch, the await() method is typically invoked by the main or coordinating thread, causing it to block until worker threads signal completion via countDown(). In contrast, CyclicBarrier has the worker threads themselves call await(), blocking them until all participating threads reach the synchronization point, leaving the main thread free to continue execution.
Another structural difference is how the counter is decremented. With CyclicBarrier, the internal count decreases automatically when a thread invokes await(). CountDownLatch requires an explicit call to countDown() from the worker thread. While CyclicBarrier can be reused after parties are released and CountDownLatch is a one-time event, reusability is a secondary characteristic rather than the core behavioral difference.
CyclicBarrier Implementation
In the following example, the main thread submits two batches of tasks consecutively. Because the barrier blocks the worker threads, the main thread is not delayed and can dispatch all tasks immediately.
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CyclicBarrierDemo {
private static final int PARTIES = 3;
private static final CyclicBarrier BARRIER = new CyclicBarrier(PARTIES);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(PARTIES * 2);
// First batch
for (int i = 0; i < PARTIES; i++) {
executor.submit(new WorkerTask("Batch1-Worker-" + i));
}
// Second batch starts immediately since main thread is not blocked
for (int i = 0; i < PARTIES; i++) {
executor.submit(new WorkerTask("Batch2-Worker-" + i));
}
executor.shutdown();
}
static class WorkerTask implements Runnable {
private final String name;
WorkerTask(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + " is processing data...");
Thread.sleep(1000); // Simulate work
System.out.println(name + " reached the barrier.");
BARRIER.await();
System.out.println(name + " proceeds after barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}
}
}CountDownLatch Implementation
Here, the main thread waits for each batch to finish before proceeding to schedule the next one. The await() call halts the coordinating thread, creating a distinct separation between the two phases of execution.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchDemo {
private static final int TASKS = 3;
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(TASKS);
// First batch
CountDownLatch latch1 = new CountDownLatch(TASKS);
for (int i = 0; i < TASKS; i++) {
executor.submit(new LatchTask("Batch1-Task-" + i, latch1));
}
latch1.await(); // Main thread blocks here
System.out.println("First batch completed. Main thread proceeds.");
// Second batch
CountDownLatch latch2 = new CountDownLatch(TASKS);
for (int i = 0; i < TASKS; i++) {
executor.submit(new LatchTask("Batch2-Task-" + i, latch2));
}
latch2.await(); // Main thread blocks here again
System.out.println("Second batch completed. Main thread proceeds.");
executor.shutdown();
}
static class LatchTask implements Runnable {
private final String name;
private final CountDownLatch latch;
LatchTask(String name, CountDownLatch latch) {
this.name = name;
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println(name + " is processing data...");
Thread.sleep(1000); // Simulate work
System.out.println(name + " finished processing.");
latch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}