Concurrency utilities: latches, semaphores and atomics
java.util.concurrent gives you tested building blocks, so you rarely need low-level wait and notify:
- Atomics (
AtomicInteger,AtomicLong,AtomicReference,LongAdder): lock-free updates such asincrementAndGetandcompareAndSet. - CountDownLatch: threads wait until a count reaches zero, for example until three caches have warmed up.
- Semaphore: limits how many threads use a resource at once, for example at most ten calls to a partner API.
- CyclicBarrier and Phaser: threads wait for each other at a checkpoint, repeatedly.
- BlockingQueue: producer-consumer hand-off;
putwaits when the queue is full andtakewaits when it's empty. - ConcurrentHashMap and CopyOnWriteArrayList: thread-safe collections.
Pick the highest-level tool that fits; it's much easier to get right than locks.
Example
ExecutorService pool = Executors.newFixedThreadPool(4);
// Wait for three warm-up tasks
CountDownLatch ready = new CountDownLatch(3);
for (String cache : List.of("courses", "users", "prices")) {
pool.submit(() -> {
try { warmUp(cache); } finally { ready.countDown(); }
});
}
ready.await(10, TimeUnit.SECONDS);
// At most two calls at a time to a rate-limited partner API
Semaphore permits = new Semaphore(2);
Runnable call = () -> {
try {
permits.acquire();
try { partnerApi.fetch(); } finally { permits.release(); }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
// Producer and consumer
BlockingQueue<String> emails = new LinkedBlockingQueue<>(100);
pool.submit(() -> { emails.put("welcome:asha@example.com"); return null; });
pool.submit(() -> { send(emails.take()); return null; });
LongAdder pageViews = new LongAdder();
pageViews.increment(); // scales better than AtomicLong under heavy contentionCommon mistake
Calling countDown() outside a finally block. If the task throws, the latch never reaches zero and await() blocks until it times out.
Under the hood
Atomics rely on the CPU's compare-and-swap instruction: read, compute, and write only if nobody changed the value in between, retrying otherwise. Under heavy contention many threads keep retrying, which is why LongAdder spreads updates across cells and sums them when read. With virtual threads, a Semaphore is the standard way to protect scarce resources such as database connections.
Check yourself
Which tool limits how many threads use a resource at once?
How this connects
Know these first
Where this leads
You've reached the end of this thread. Try a learning path for what's next.
Part of Crack the Java interview, Upgrade from Java 8 to Java 25.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.