Source: Jira KAN-159 and KAN-160, both titled "CountDownLatch in Java" (duplicates, handled as one article).
KAN-159 points to Baeldung's
Abbreviations
| Abbreviation | Full name |
|---|---|
| JUC | java.util.concurrent package |
| AQS | AbstractQueuedSynchronizer |
| JDK | Java Development Kit |
| CAS | Compare-And-Swap |
| API | Application Programming Interface |
What it is
java.util.concurrent.CountDownLatch (in JUC since JDK 5) is a synchronizer that lets one or more threads wait
until a set of operations performed by other threads has completed. Think of it as a counter that only goes down:
| Method | Behaviour |
|---|---|
new CountDownLatch(int count) |
Starts at count; a negative value throws IllegalArgumentException |
void countDown() |
Decrements the count; when it reaches 0, releases every waiting thread. No-op at 0 |
void await() |
Blocks until the count is 0 (or the thread is interrupted) |
boolean await(long timeout, TimeUnit unit) |
As above, but returns false if the timeout elapses first |
long getCount() |
Current count, for diagnostics |
Two properties shape how it is used:
- It is one-shot. Once the count hits zero it cannot be reset; later
await()calls return immediately.
Need a reusable barrier? UseCyclicBarrierorSemaphore. - Anyone can count down. There is no ownership (unlike a lock), so the thread that calls
countDown()does
not have to be the one that will be waited on.
Internally it is a thin wrapper over AQS (AbstractQueuedSynchronizer) in shared mode: the AQS state is the
count, countDown() decrements it with CAS (Compare-And-Swap), and reaching zero releases all queued waiters at
once.
#mermaid-svg-7fSoKxQoq3zT7fPF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7fSoKxQoq3zT7fPF .error-icon{fill:#552222;}#mermaid-svg-7fSoKxQoq3zT7fPF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7fSoKxQoq3zT7fPF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7fSoKxQoq3zT7fPF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7fSoKxQoq3zT7fPF .marker.cross{stroke:#333333;}#mermaid-svg-7fSoKxQoq3zT7fPF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7fSoKxQoq3zT7fPF p{margin:0;}#mermaid-svg-7fSoKxQoq3zT7fPF defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-7fSoKxQoq3zT7fPF g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-7fSoKxQoq3zT7fPF g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-7fSoKxQoq3zT7fPF g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-7fSoKxQoq3zT7fPF g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-7fSoKxQoq3zT7fPF g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-7fSoKxQoq3zT7fPF .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-7fSoKxQoq3zT7fPF .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-7fSoKxQoq3zT7fPF .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-7fSoKxQoq3zT7fPF .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7fSoKxQoq3zT7fPF .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-7fSoKxQoq3zT7fPF .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-7fSoKxQoq3zT7fPF .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-7fSoKxQoq3zT7fPF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7fSoKxQoq3zT7fPF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7fSoKxQoq3zT7fPF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7fSoKxQoq3zT7fPF .edgeLabel .label text{fill:#333;}#mermaid-svg-7fSoKxQoq3zT7fPF .label div .edgeLabel{color:#333;}#mermaid-svg-7fSoKxQoq3zT7fPF .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-7fSoKxQoq3zT7fPF .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-7fSoKxQoq3zT7fPF .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-7fSoKxQoq3zT7fPF .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-7fSoKxQoq3zT7fPF .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-7fSoKxQoq3zT7fPF .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7fSoKxQoq3zT7fPF .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7fSoKxQoq3zT7fPF #statediagram-barbEnd{fill:#333333;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7fSoKxQoq3zT7fPF .cluster-label,#mermaid-svg-7fSoKxQoq3zT7fPF .nodeLabel{color:#131300;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-7fSoKxQoq3zT7fPF .note-edge{stroke-dasharray:5;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-note text{fill:black;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram-note .nodeLabel{color:black;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagram .edgeLabel{color:red;}#mermaid-svg-7fSoKxQoq3zT7fPF #dependencyStart,#mermaid-svg-7fSoKxQoq3zT7fPF #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-7fSoKxQoq3zT7fPF .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7fSoKxQoq3zT7fPF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} new CountDownLatch(n)
countDown() while count > 1
countDown() makes count 0
countDown() is a no-op
Counting
Open
await() blocks here
await() returns at once
Pattern 1 --- wait for N tasks to finish
The main thread creates a latch with the number of tasks, each worker counts down when it finishes, and the
main thread awaits.
java
public class Worker implements Runnable {
private final List<String> output;
private final CountDownLatch done;
Worker(List<String> output, CountDownLatch done) {
this.output = output;
this.done = done;
}
@Override
public void run() {
try {
doSomeWork();
output.add("Counted down");
} finally {
done.countDown(); // always, even if the work throws
}
}
}
java
@Test
void whenParallelProcessing_thenMainThreadWillBlockUntilCompletion() throws InterruptedException {
List<String> output = Collections.synchronizedList(new ArrayList<>());
CountDownLatch done = new CountDownLatch(5);
List<Thread> workers = Stream.generate(() -> new Thread(new Worker(output, done)))
.limit(5)
.toList();
workers.forEach(Thread::start);
done.await(); // main thread blocks here
output.add("Latch released");
assertThat(output).containsExactly(
"Counted down", "Counted down", "Counted down", "Counted down", "Counted down",
"Latch released");
}
Pattern 2 --- start N threads at the same moment
Flip the roles: a latch of 1 acts as a starting gun. Every worker awaits it, and the main thread releases them
all at once. Combine it with a second latch to also wait for completion.
doneSignal (N) Workers x N startSignal (1) Main thread doneSignal (N) Workers x N startSignal (1) Main thread #mermaid-svg-y9SKW4xbPKUIXy3f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y9SKW4xbPKUIXy3f .error-icon{fill:#552222;}#mermaid-svg-y9SKW4xbPKUIXy3f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y9SKW4xbPKUIXy3f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y9SKW4xbPKUIXy3f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y9SKW4xbPKUIXy3f .marker.cross{stroke:#333333;}#mermaid-svg-y9SKW4xbPKUIXy3f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y9SKW4xbPKUIXy3f p{margin:0;}#mermaid-svg-y9SKW4xbPKUIXy3f .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y9SKW4xbPKUIXy3f text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-y9SKW4xbPKUIXy3f .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-y9SKW4xbPKUIXy3f .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-y9SKW4xbPKUIXy3f .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-y9SKW4xbPKUIXy3f .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-y9SKW4xbPKUIXy3f #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-y9SKW4xbPKUIXy3f .sequenceNumber{fill:white;}#mermaid-svg-y9SKW4xbPKUIXy3f #sequencenumber{fill:#333;}#mermaid-svg-y9SKW4xbPKUIXy3f #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-y9SKW4xbPKUIXy3f .messageText{fill:#333;stroke:none;}#mermaid-svg-y9SKW4xbPKUIXy3f .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y9SKW4xbPKUIXy3f .labelText,#mermaid-svg-y9SKW4xbPKUIXy3f .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-y9SKW4xbPKUIXy3f .loopText,#mermaid-svg-y9SKW4xbPKUIXy3f .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-y9SKW4xbPKUIXy3f .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-y9SKW4xbPKUIXy3f .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-y9SKW4xbPKUIXy3f .noteText,#mermaid-svg-y9SKW4xbPKUIXy3f .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-y9SKW4xbPKUIXy3f .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y9SKW4xbPKUIXy3f .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y9SKW4xbPKUIXy3f .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-y9SKW4xbPKUIXy3f .actorPopupMenu{position:absolute;}#mermaid-svg-y9SKW4xbPKUIXy3f .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-y9SKW4xbPKUIXy3f .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-y9SKW4xbPKUIXy3f .actor-man circle,#mermaid-svg-y9SKW4xbPKUIXy3f line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-y9SKW4xbPKUIXy3f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} all parked, ready start N threads await() countDown() released together do work concurrently countDown() each await() released when count = 0
java
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; i++) {
new Thread(() -> {
try {
startSignal.await(); // wait for the gun
hitTheService();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
doneSignal.countDown();
}
}).start();
}
startSignal.countDown(); // everyone goes now
doneSignal.await();
This is the classic way to provoke race conditions in a test: without the start gun, threads start one by one and
rarely overlap.
Pattern 3 --- do not wait forever
If a worker dies before counting down, a plain await() blocks forever. Production code should use the timed
variant and decide what a timeout means.
java
boolean completed = done.await(3, TimeUnit.SECONDS);
if (!completed) {
throw new IllegalStateException("Timed out; " + done.getCount() + " task(s) still running");
}
A realistic use: parallel warm-up during Spring Boot startup
An ApplicationRunner runs after Tomcat has started but before the application reports ready (see
Spring Boot Startup Lifecycle). Warming caches there in
parallel, and failing startup if they don't finish in time, keeps a half-warm pod out of the load balancer.
java
@Component
public class CacheWarmUpRunner implements ApplicationRunner {
private final List<CacheLoader> loaders;
public CacheWarmUpRunner(List<CacheLoader> loaders) {
this.loaders = loaders;
}
@Override
public void run(ApplicationArguments args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(loaders.size());
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) { // Java 21+
for (CacheLoader loader : loaders) {
pool.submit(() -> {
try {
loader.load();
} finally {
latch.countDown();
}
});
}
if (!latch.await(30, TimeUnit.SECONDS)) {
pool.shutdownNow(); // interrupt stragglers, otherwise close() would wait for them
throw new IllegalStateException("Cache warm-up timed out, remaining=" + latch.getCount());
}
}
}
}
ExecutorService.close() (Java 19+) waits for submitted tasks to finish, so the timeout path must interrupt them
first --- and the loaders must respond to interruption for the timeout to be real.
Note that the latch only reports completion , not success : a loader that throws still counts down. If failures
matter, collect them (e.g. in a ConcurrentLinkedQueue) or use Futures --- see the comparison below.
Pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
countDown() not in finally |
An exception leaves the count above 0; await() hangs |
Always count down in finally |
Untimed await() in production |
A lost worker hangs the caller forever | await(timeout, unit) and handle false |
| Count does not match the number of tasks | Too high: hang. Too low: caller proceeds early | Derive the count from the task list |
Swallowing InterruptedException |
Cancellation is lost | Restore with Thread.currentThread().interrupt() or propagate |
| Treating completion as success | Failed tasks look done | Collect errors or use Future/CompletableFuture |
| Trying to reuse the latch | Second round never blocks | Create a new latch, or use CyclicBarrier |
Thread.sleep in tests instead of a latch |
Slow and flaky tests | Count down from the async callback and await with a timeout |
Choosing a tool
| Tool | Semantics | Reusable | Carries results/errors |
|---|---|---|---|
CountDownLatch |
Wait until count reaches 0 | No | No |
CyclicBarrier |
N parties wait for each other, then all proceed | Yes (resets) | No |
Semaphore |
At most N concurrent permits | Yes | No |
Phaser |
Like a barrier with dynamic party registration and phases | Yes | No |
ExecutorService.invokeAll |
Submit and wait for a batch of Callables |
n/a | Yes (Future) |
CompletableFuture.allOf |
Compose async results | n/a | Yes |
Rule of thumb: use CountDownLatch when you need a simple "wait for these N events" signal that crosses thread
boundaries you do not control (callbacks, listeners, test hooks). When you own the tasks, invokeAll or
CompletableFuture.allOf also give you their results and exceptions.
Related notes:
- The same permit concept in Python: threading.Semaphore.
- For reactive code, compose publishers (
Flux.merge(...).then()) instead of blocking:
Mono vs Flux.
Conclusion
CountDownLatch is the smallest useful coordination primitive in JUC: a one-shot, ownerless countdown that
releases all waiters at zero. Two patterns cover almost every use --- wait for N to finish and start N at once ---
and three habits make it safe: count down in finally, await with a timeout, and never mistake completion for
success.
References
- Jira KAN-159, KAN-160 (geek-chow.atlassian.net)
- Baeldung --- Guide to CountDownLatch in Java
- Javadoc ---
CountDownLatch(Java 21) - Brian Goetz et al., Java Concurrency in Practice, §5.5 Synchronizers