Advanced Thread Coordination Utilities in Java

01 Aug 2026 10 min read
1
In this chapter, we will explore five important synchronization utilities: CountDownLatch, CyclicBarrier, Phaser, Semaphore, and Exchanger.

Each utility addresses a different coordination problem and plays an important role in building scalable concurrent applications.

CountDownLatch

A CountDownLatch allows one or more threads to wait until a specified number of operations have completed.

It is initialized with a count, and each completed operation decreases that count. Once the count reaches zero, all waiting threads are released automatically.

Unlike locks, a CountDownLatch is a one-time synchronization aid. After the count reaches zero, it cannot be reset or reused.

Creating a CountDownLatch

A CountDownLatch is created by specifying the number of operations that must complete before waiting threads can proceed.
CountDownLatch latch = new CountDownLatch(3);
Here, the latch starts with a count of three, so three calls to countDown() are required before waiting threads are released.

Worker threads signal task completion by invoking countDown().
latch.countDown();
Each invocation decreases the internal counter by one. When the count reaches zero, all waiting threads are released.

Threads that need to wait invoke the await() method.
latch.await();
If the count is greater than zero, the calling thread blocks until it reaches zero. Otherwise, await() returns immediately.

Like many blocking methods in the concurrency API, await() throws an InterruptedException if the waiting thread is interrupted.

Complete Example

The following program waits until three worker threads complete their work before allowing the main thread to continue.
import java.util.concurrent.CountDownLatch;

public class LatchExample {
    public static void main(String[] args) throws InterruptedException {

        CountDownLatch latch = new CountDownLatch(3);

        Runnable worker = () -> {
            System.out.println(Thread.currentThread().getName() + " finished");
            latch.countDown();
        };

        new Thread(worker, "Worker-1").start();
        new Thread(worker, "Worker-2").start();
        new Thread(worker, "Worker-3").start();

        latch.await();

        System.out.println("All workers completed");
    }
}
Possible output:
Worker-1 finished
Worker-3 finished
Worker-2 finished
All workers completed
Notice that the main thread remains blocked until every worker has called countDown().

The order in which the workers finish is irrelevant. Only when the count reaches zero does the main thread resume execution.

Applications of CountDownLatch

A CountDownLatch is useful whenever one thread must wait for a fixed number of independent tasks to complete before continuing.

During application startup, it can delay request processing until essential services such as database connections, caches, and messaging systems have finished initializing.

In parallel processing, it enables a coordinating thread to wait for several worker threads to complete their computations before combining the results.

It is also widely used in integration and concurrency testing, where test code must wait until multiple background operations have finished before verifying the final outcome.

Limitations of CountDownLatch

Although CountDownLatch is simple and efficient, it has one important limitation: it is designed for one-time use.

Once the counter reaches zero, the latch cannot be reset.

If the same synchronization point is needed repeatedly, a different coordination utility such as CyclicBarrier or Phaser should be used instead.

CyclicBarrier

While a CountDownLatch allows one or more threads to wait until a fixed number of operations complete, a CyclicBarrier is designed for situations where multiple threads must wait for each other before continuing.

Instead of one thread waiting for several worker threads, every participating thread waits at a common synchronization point called a barrier. Once all participating threads reach the barrier, they are released together and continue executing.

Unlike CountDownLatch, a CyclicBarrier is reusable. After all waiting threads cross the barrier, it automatically resets itself and can be used again for the next synchronization cycle.

Creating a CyclicBarrier

A CyclicBarrier is created by specifying the number of participating threads.
CyclicBarrier barrier = new CyclicBarrier(3);
Here, the barrier waits until three threads invoke await().
barrier.await();
Each thread blocks at the barrier until all participating threads arrive.

Once the required number of threads has reached the barrier, they are released simultaneously and continue executing together.

Complete Example

import java.util.concurrent.CyclicBarrier;

public class BarrierExample {
    public static void main(String[] args) {

        CyclicBarrier barrier = new CyclicBarrier(3);

        Runnable worker = () -> {
            try {
                System.out.println(Thread.currentThread().getName() + " completed work");
                barrier.await();
                System.out.println(Thread.currentThread().getName() + " continues");
            } catch (Exception e) {
                e.printStackTrace();
            }
        };

        new Thread(worker, "Thread-1").start();
        new Thread(worker, "Thread-2").start();
        new Thread(worker, "Thread-3").start();
    }
}
Possible output:
Thread-1 completed work
Thread-2 completed work
Thread-3 completed work

Thread-3 continues
Thread-1 continues
Thread-2 continues
Notice that none of the threads continue until every participating thread has reached the barrier.

Barrier Action

A CyclicBarrier can optionally execute a task immediately after the last thread reaches the barrier and before the waiting threads are released.
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads arrived"));
When the third thread reaches the barrier, the barrier action executes once, after which all waiting threads continue normally.

Applications of CyclicBarrier

A CyclicBarrier is commonly used in simulations, scientific computing, parallel algorithms, multiplayer game engines, and distributed processing systems where multiple worker threads repeatedly perform independent work before synchronizing at a common point.

Because the barrier automatically resets after each synchronization cycle, it is well suited for iterative computations consisting of multiple execution phases.

Phaser

While CyclicBarrier is reusable, the number of participating threads must be fixed when the barrier is created. Modern applications, however, often have threads joining or leaving dynamically during execution.

To support these scenarios, Java provides the Phaser class.

A Phaser extends the concept of a reusable barrier by supporting multiple execution phases and allowing threads to register or deregister dynamically.

Creating a Phaser

A Phaser is created by specifying the number of initial participants.
Phaser phaser = new Phaser(3);
Unlike CyclicBarrier, additional threads may register later if necessary. The most commonly used method is:
phaser.arriveAndAwaitAdvance();
This method performs two operations simultaneously. The calling thread signals that it has completed the current phase and then waits until every other registered participant also completes that phase.

Once all participants arrive, the phaser advances to the next phase and releases every waiting thread.

Complete Example

import java.util.concurrent.Phaser;

public class PhaserExample {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(3);

        Runnable worker = () -> {
            System.out.println(Thread.currentThread().getName() + " Phase 1");
            phaser.arriveAndAwaitAdvance();

            System.out.println(Thread.currentThread().getName() + " Phase 2");
            phaser.arriveAndAwaitAdvance();

            System.out.println(Thread.currentThread().getName() + " Finished");
        };

        new Thread(worker, "Thread-1").start();
        new Thread(worker, "Thread-2").start();
        new Thread(worker, "Thread-3").start();
    }
}
All three threads complete Phase 1 before any thread begins Phase 2. Likewise, Phase 2 finishes only after every thread reaches the second synchronization point.

Dynamic Registration

One of the biggest advantages of a Phaser is its ability to add or remove participants while the program is running. New participants can join using:
phaser.register();
When a thread permanently finishes its work, it can leave the phaser using:
phaser.arriveAndDeregister();
This flexibility makes Phaser particularly useful for dynamic workloads where the number of participating threads cannot be determined in advance.

Semaphore

Unlike synchronization mechanisms that coordinate the execution order of threads, a Semaphore controls how many threads are allowed to access a shared resource at the same time.

Instead of providing mutual exclusion like a lock, a semaphore maintains a fixed number of permits.

A thread must acquire a permit before accessing the protected resource and release the permit when it finishes. If no permits are available, the thread automatically waits until another thread releases one.

Semaphores are particularly useful when a limited number of identical resources can be shared safely among multiple threads, such as database connections, printers, or network connections.

Creating a Semaphore

A semaphore is created by specifying the number of available permits.
Semaphore semaphore = new Semaphore(3);
Here, at most three threads may access the protected resource simultaneously. Additional threads automatically wait until a permit becomes available.

Before accessing the shared resource, a thread acquires a permit.
semaphore.acquire(); 
If a permit is available, it is immediately assigned to the thread. Otherwise, the thread blocks until another thread releases a permit.

Once the thread finishes using the shared resource, it should always release its permit.
semaphore.release();
Releasing a permit allows another waiting thread to continue.

Complete Example

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) {
        Runnable worker = () -> {
            try {
                semaphore.acquire();

                System.out.println(Thread.currentThread().getName() + " acquired permit");

                Thread.sleep(2000);

                System.out.println(Thread.currentThread().getName() + " releasing permit");

                semaphore.release();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        for (int i = 1; i <= 5; i++) {
            new Thread(worker, "Thread-" + i).start();
        }
    }
}
Possible output:
Thread-1 acquired permit
Thread-2 acquired permit

Thread-1 releasing permit
Thread-3 acquired permit

Thread-2 releasing permit
Thread-4 acquired permit

...
Notice that although five threads are created, only two execute the protected section simultaneously because only two permits are available.

Applications of Semaphore

Semaphores are commonly used to limit concurrent access to finite resources. Database connection pools often allow only a fixed number of active connections, preventing excessive load on the database server.

Web servers and API gateways use semaphores to implement rate limiting, ensuring that only a certain number of requests are processed concurrently.

Other common applications include printer management, thread pools, resource throttling, and controlling access to expensive hardware devices.

Exchanger

An Exchanger is a synchronization utility that allows two threads to exchange data directly with one another.

Each thread provides an object to the exchanger and waits until the other thread also arrives. Once both threads reach the exchange point, the objects are swapped, and both threads continue execution.

Unlike queues, where producers and consumers communicate through an intermediate data structure, an Exchanger enables direct, synchronized data exchange between exactly two participating threads.

Creating an Exchanger

The generic type specifies the type of object that participating threads exchange.
Exchanger exchanger = new Exchanger<>(); 
The exchange() method performs the synchronized data exchange.
String received = exchanger.exchange(data);
If the other thread has not yet arrived, the calling thread waits. Once both threads invoke exchange(), the objects are exchanged, and both threads continue execution.

Complete Example

import java.util.concurrent.Exchanger;

public class ExchangerExample {
    public static void main(String[] args) {

        Exchanger<String> exchanger = new Exchanger<>();

        Thread producer = new Thread(() -> {
            try {
                String response = exchanger.exchange("Data from Producer");
                System.out.println(response);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                String response = exchanger.exchange("Data from Consumer");
                System.out.println(response);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
    }
}
Possible output:
Data from Consumer
Data from Producer
Each thread receives the object supplied by the other thread.

Applications of Exchanger

An Exchanger is useful whenever two threads repeatedly exchange data during processing.

It is commonly used in pipeline-based applications where one thread produces data while another consumes and transforms it.

It can also be used in genetic algorithms, simulation engines, and double-buffering techniques, where two worker threads periodically swap buffers to avoid unnecessary object creation and synchronization overhead.

Conclusion

Java's high-level synchronization utilities provide powerful abstractions for coordinating threads beyond simple mutual exclusion.

CountDownLatch, CyclicBarrier, Phaser, Semaphore, and Exchanger each solve a different coordination problem, enabling developers to build scalable concurrent applications without relying on low-level monitor methods such as wait() and notify().

Choosing the right utility simplifies concurrent code, improves readability, and reduces the likelihood of synchronization bugs.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion