Java CountDownLatch: Coordinating Concurrent Tasks
java countdownlatch: Learn how to use Java CountDownLatch to coordinate threads, handle timeouts, and avoid common pitfalls in concurrent code.
When you need several worker threads to start only after a set of prerequisites complete, Java CountDownLatch provides a simple counting gate. Unlike a barrier, it is a one-shot latch: once the count reaches zero, all waiting threads are released and the latch cannot be reused. This makes it ideal for scenarios like waiting for a service to initialize or for a batch of tasks to finish before proceeding.
How CountDownLatch Works
A CountDownLatch is initialized with a positive integer count. Threads call await() to block until the count reaches zero, while other threads call countDown() to decrement the count. When the count hits zero, all blocked threads are released atomically. The latch does not reset, so it is not suitable for repeated synchronization.
CountDownLatch latch = new CountDownLatch(2); // Thread A latch.countDown(); // Thread B latch.await(); // blocks until count is zero
The constructor throws IllegalArgumentException if the count is negative. The countDown() method never blocks and can be called multiple times by the same thread, but calling it more than the initial count has no effect beyond reaching zero.
Startup Coordination Example
A common use case is ensuring that worker threads do not start processing until a shared resource, such as a database connection pool or a configuration cache, is ready. The main thread sets up the resource and then releases the workers.
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class StartupExample { public static void main(String[] args) throws InterruptedException { int workerCount = 3; CountDownLatch readyLatch = new CountDownLatch(1); ExecutorService executor = Executors.newFixedThreadPool(workerCount); for (int i = 0; i < workerCount; i++) { executor.submit(() -> { try { readyLatch.await(); // wait for the signal System.out.println("Worker started"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // Simulate resource initialization Thread.sleep(1000); readyLatch.countDown(); // release all workers executor.shutdown(); } }
Here, the latch count is 1. All workers block on await() until the main thread calls countDown() after initialization. This pattern is simple and avoids busy-waiting.
Handling Timeouts and Interruptions
Blocking indefinitely on await() can leave threads stuck if a countDown() is never called. Use the timed variant to avoid this:
boolean completed = latch.await(5, TimeUnit.SECONDS); if (!completed) { // handle timeout }
The method returns true if the count reached zero, false if the timeout elapsed first. Always handle InterruptedException by restoring the interrupt status:
try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // decide whether to abort or continue }
This is critical in production code where threads may be shut down gracefully.
Common Misuse and Pitfalls
One frequent mistake is forgetting to call countDown() in a finally block when the counted-down operation can throw an exception. If the count never reaches zero, all waiting threads block forever. Always ensure countDown() is executed, even on failure.
Another issue is attempting to reuse a CountDownLatch. Once the count reaches zero, await() returns immediately and countDown() has no effect. For cyclic synchronization, use CyclicBarrier instead.
Also, be careful about the initial count. If you set it to the number of worker threads and each worker counts down at the end, but one worker fails to start, the latch never opens. This is why timeouts are essential.
CountDownLatch vs CyclicBarrier
Both primitives coordinate threads, but they serve different purposes. CountDownLatch is a one-shot gate that threads wait on; CyclicBarrier is a reusable rendezvous point where threads wait for each other.
| Feature | CountDownLatch | CyclicBarrier |
|---|---|---|
| Reusability | No | Yes |
| Primary use | Wait for a set of events | Wait for a set of threads |
| Countdown source | Any thread can call countDown() | All parties must arrive |
| Action on reset | Not possible | reset() or automatic with barrier action |
| Exception handling | await() throws InterruptedException | await() throws BrokenBarrierException |
Use CountDownLatch when you need to wait for an external condition to become true. Use CyclicBarrier when multiple threads must synchronize at a common point and then proceed together.
Performance and Operational Considerations
CountDownLatch is lightweight; it uses a simple AQS-based state and does not spin. However, blocking threads consume OS resources and may cause thread-pool exhaustion if the latch never opens. In high-throughput systems, prefer timeouts and monitor the number of waiting threads.
Avoid calling countDown() while holding a lock or inside a critical section if the operation is expensive. The latch itself does not require synchronization, but the surrounding logic might. Also, be aware that await() releases the CPU, so it is not suitable for ultra-low-latency spin-wait scenarios.
For production observability, log when the latch is created, when each countDown() occurs, and when threads are released. This helps diagnose deadlocks or slow initialization.
Choosing the Right Coordination Primitive
The decision between CountDownLatch, CyclicBarrier, Phaser, and other tools depends on the coordination pattern. CountDownLatch is the right choice when you have a one-time event that must happen before other threads proceed. If you need repeated synchronization or dynamic party counts, consider Phaser. If threads must wait for each other at a barrier, use CyclicBarrier.
A practical pattern is combining CountDownLatch with an ExecutorService to wait for all submitted tasks to complete. While Future.get() can achieve this, a latch allows you to wait without collecting results, which is useful when tasks are fire-and-forget but you need to know they finished.
CountDownLatch doneLatch = new CountDownLatch(tasks.size()); for (Runnable task : tasks) { executor.submit(() -> { try { task.run(); } finally { doneLatch.countDown(); } }); } if (!doneLatch.await(30, TimeUnit.SECONDS)) { // handle incomplete tasks }
This approach gives you a clean way to wait for completion with a timeout, without keeping references to Future objects. It also works when tasks are submitted from multiple threads.
Remember that CountDownLatch is a tool, not a solution for every concurrency problem. Use it where its one-shot semantics fit naturally, and always pair blocking calls with timeouts and interrupt handling to keep your application responsive.