CountDownLatch and Semaphore in Java
Java’s concurrency utilities include two primitives that block threads waiting for specific conditions: CountDownLatch for one-shot event coordination and Semaphore for resource access control. They solve different problems but share a pattern — they put threads to sleep until something external makes them wake up.
CountDownLatch
A CountDownLatch starts at some positive count. Threads call await() and block until the count reaches zero, which happens when other threads call countDown(). The key properties are:
- It is one-shot — once the count hits zero, it stays there. You can’t reset it; you need a new instance.
- Any thread calling
await()after the count is already zero returns immediately (no blocking). - All waiting threads are released simultaneously when the count reaches zero.
This makes it ideal for “wait until N tasks complete” patterns — a setup phase, a startup gate, or any scenario where you need to know that a set of operations have all finished before proceeding.
The code
The demo has three parts: basic countdown (waiting for tasks), one-shot signaling, and a barrier pattern where multiple threads coordinate through the latch.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// Part 1: Waiter waits for 3 parallel tasks
System.out.println("--- Part 1: Waiter waiting for 3 tasks to finish ---");
CountDownLatch latch = new CountDownLatch(3);
long start = System.nanoTime();
ExecutorService workers = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 3; i++) {
final int taskNum = i;
workers.submit(() -> {
try {
long sleepMs = (long) (Math.random() * 500) + 100;
Thread.sleep(sleepMs);
System.out.println(" Task " + taskNum + " completed in ~" + sleepMs + "ms");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
System.out.println(" Waiter: all tasks submitted, now blocking...");
latch.await(); // blocks until count == 0
long elapsed = (System.nanoTime() - start) / 1_000_000;
System.out.println(" Waiter: ALL TASKS DONE. Elapsed: ~" + elapsed + "ms");
workers.shutdown();
workers.awaitTermination(10, TimeUnit.SECONDS);
// Part 2: One-shot signaling
System.out.println("--- Part 2: Latch is one-shot ---");
CountDownLatch phaseLatch = new CountDownLatch(1);
Thread starter = new Thread(() -> {
try { Thread.sleep(300); phaseLatch.countDown(); } catch (InterruptedException e) {}
});
starter.start();
long start2 = System.nanoTime();
phaseLatch.await();
System.out.println(" Waiter: got the signal. Elapsed wait: ~" + ((System.nanoTime() - start2) / 1_000_000) + "ms");
starter.join();
// Part 3: Barrier — all workers reach this point together
System.out.println("--- Part 3: Barrier coordination ---");
CountDownLatch barrier = new CountDownLatch(2);
Thread t1 = new Thread(() -> {
try { Thread.sleep(200); barrier.countDown(); } catch (InterruptedException e) {}
});
Thread t2 = new Thread(() -> {
try { Thread.sleep(400); barrier.countDown(); } catch (InterruptedException e) {}
});
t1.start(); t2.start();
t1.join(); t2.join();
}
}
Running it
In Part 1, the three worker threads each ran between ~400 and ~490ms. The waiter elapsed ~495ms — slightly more than the longest task — because it blocked until all three tasks had counted down, regardless of who finished first. The latch doesn’t care about order; it only cares that the count reached zero.
In Part 2, the starter thread slept for 300ms before calling countDown(). The waiter’s elapsed wait was ~301ms, confirming it was blocked exactly until the signal arrived — and once it did, it resumed instantly. If a second thread called await() on the same latch at that point, it would return immediately since the count is already zero.
In Part 3, Worker-A reached the barrier after ~200ms and counted it down to 1, then waited because it was already at 1, not 0. Worker-B arrived ~400ms in, counted down to 0, which released both threads. The barrier ensures no thread proceeds past it until every participant has arrived — useful for phased parallel workloads.
Semaphore
A Semaphore manages a fixed pool of permits. Threads call acquire() and block if no permits are available; they call release() when done to return the permit. It’s the classic resource limiter: connection pools, rate limiters, or any bounded concurrency control.
- Fair mode (
new Semaphore(n, true)) uses FIFO ordering — the thread that waited longest gets the permit first. - Unfair mode (the default) doesn’t guarantee order, which can improve throughput but risks starvation under heavy contention.
tryAcquire()attempts non-blocking acquisition — returnsfalseimmediately if no permits are available.
The code
This demo exercises all three acquisition modes: blocking acquire with a permit pool, fair ordering, and non-blocking tryAcquire.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class SemaphoreDemo {
public static void main(String[] args) throws InterruptedException {
// Part 1: Connection pool with limit of 2
System.out.println("--- Part 1: Connection pool with limit of 2 ---");
int maxConnections = 2;
Semaphore connectionPool = new Semaphore(maxConnections);
ExecutorService pool = Executors.newFixedThreadPool(6);
AtomicInteger peakActive = new AtomicInteger(0);
for (int i = 1; i <= 6; i++) {
final int connId = i;
pool.submit(() -> {
try {
connectionPool.acquire(); // blocks if no permits
System.out.println(" Connection " + connId + ": acquired!");
Thread.sleep((long) (Math.random() * 800) + 200);
System.out.println(" Connection " + connId + ": releasing");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
connectionPool.release();
}
});
}
pool.shutdown();
pool.awaitTermination(15, TimeUnit.SECONDS);
// Part 2: Fair ordering
System.out.println("--- Part 2: Fair semaphore (FIFO) ---");
Semaphore fairSema = new Semaphore(1, true);
Thread f1 = new Thread(() -> {
try { fairSema.acquire(); System.out.println(" [A] acquired! Holding for 1s..."); Thread.sleep(1000); } catch (InterruptedException e) {}
finally { fairSema.release(); System.out.println(" [A] released."); }
});
Thread f2 = new Thread(() -> {
try { fairSema.acquire(); System.out.println(" [B] acquired! Holding for 1s..."); Thread.sleep(1000); } catch (InterruptedException e) {}
finally { fairSema.release(); System.out.println(" [B] released."); }
});
f1.start(); Thread.sleep(50);
f2.start();
f1.join(); f2.join();
// Part 3: Try-acquire (non-blocking)
System.out.println("--- Part 3: Try-acquire ---");
Semaphore smallPool = new Semaphore(1);
System.out.println(" Thread 1 acquired: " + smallPool.tryAcquire());
System.out.println(" Thread 2 tryAcquire (should be false): " + smallPool.tryAcquire());
smallPool.release();
System.out.println(" After release, Thread 2 tryAcquire: " + smallPool.tryAcquire());
}
}
Running it
Part 1 shows six connection requests competing for only two permits. Connections acquired and released in pairs — the output confirms that only two connections were active at any time. No request was lost; blocked threads simply waited until a permit freed up.
In Part 2, Worker-A started first (50ms head start). With fair=true, it held the permit for a full second before releasing, and only then did Worker-B acquire — FIFO order is guaranteed. Swap to false (unfair mode) and you might see Worker-B acquire before Worker-A despite arriving later, because the JVM’s scheduler may wake an arbitrary waiting thread.
Part 3 demonstrates non-blocking acquisition: the first call gets the permit, the second immediately returns false without blocking, and after a manual release, the third call succeeds. This is useful when you’d rather skip work than wait — for example, rejecting a request outright instead of queueing it indefinitely.
Takeaway
Use CountDownLatch when you need to wait for an event (all threads finish, initialization completes) — it’s a gate that opens once. Use Semaphore when you need to limit concurrency (connection pools, rate limiters) — it’s a token bucket that controls how many threads can proceed at once.