Stage 8: Concurrency and the JVM, lesson 3 of 7

Concurrency utilities: latches, semaphores and atomics

Advanced3 min read@since 9Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

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 as incrementAndGet and compareAndSet.
  • 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; put waits when the queue is full and take waits 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

Java
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 contention

Common 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

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.