Free lesson · Concurrency Interview Mastery
The java.util.concurrent Package Tour
java.util.concurrent (everyone says "j.u.c") is one of the best-designed standard libraries in any language. It gives you battle-tested, high-performance concurrency building blocks so you almost never have to hand-roll a thread-safe data structure or a synchronizer. This lesson is a map of what's in it, organized by category, with the rule that matters most stated up front: before you write your own concurrent primitive, check whether j.u.c already has it. It almost always does, and theirs is more correct than yours will be.
Here's the territory at a glance:
mindmap
root((java.util.concurrent))
Executors
ExecutorService
ThreadPoolExecutor
ScheduledExecutorService
ForkJoinPool
Concurrent Collections
ConcurrentHashMap
CopyOnWriteArrayList
BlockingQueue family
ConcurrentLinkedQueue
Atomics
AtomicInteger / Long
AtomicReference
LongAdder
Synchronizers
CountDownLatch
CyclicBarrier
Semaphore
Phaser
Locks
ReentrantLock
ReadWriteLock
StampedLock
Futures
Future
CompletableFuture
This mind map shows the major categories of the java.util.concurrent package. There are six branches. Executors include ExecutorService, ThreadPoolExecutor, ScheduledExecutorService, and ForkJoinPool. Concurrent collections include ConcurrentHashMap, CopyOnWriteArrayList, the BlockingQueue family, and ConcurrentLinkedQueue. Atomics include AtomicInteger, AtomicLong, AtomicReference, and LongAdder. Synchronizers include CountDownLatch, CyclicBarrier, Semaphore, and Phaser. Locks include ReentrantLock, ReadWriteLock, and StampedLock. Futures include Future and CompletableFuture.
Let's walk each category.
Executors - managing threads
Covered in the threading lesson, but to place them in the map: the Executors factory and ExecutorService interface let you submit tasks to managed thread pools instead of creating raw threads.
ThreadPoolExecutor- the configurable workhorse behind most pools (core/max size, queue, rejection policy).ScheduledExecutorService- delayed and periodic task execution.ForkJoinPool- work-stealing pool for divide-and-conquer parallelism; also powers parallel streams.
ExecutorService pool = Executors.newFixedThreadPool(8);
pool.submit(() -> doWork());
pool.shutdown();
Concurrent collections - thread-safe data structures
Never share a plain HashMap or ArrayList across threads without external locking. Use these instead - they're designed for concurrent access and far outperform wrapping a plain collection in synchronized.
ConcurrentHashMap- the one you'll use most. A thread-safe map that allows high concurrency by locking only small segments rather than the whole map. Has atomic helpers likecompute,computeIfAbsent,mergefor safe read-modify-write on entries.CopyOnWriteArrayList- thread-safe list optimized for read-heavy, write-rare workloads; every write copies the whole array, so reads need no locking at all. Great for things like listener lists; terrible for write-heavy use.BlockingQueuefamily - queues wheretake()blocks when empty andput()blocks when full. The backbone of producer-consumer designs:ArrayBlockingQueue(bounded),LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue(zero-capacity handoff).ConcurrentLinkedQueue- a non-blocking, lock-free unbounded queue for high-throughput, non-blocking handoff.
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("key", 1, Integer::sum); // atomic increment of a map value
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
queue.put(task); // blocks if full
Task t = queue.take(); // blocks if empty
Tip:
ConcurrentHashMap.computeIfAbsentandmergeare the idiomatic fix for check-then-act races on maps (the "get, check null, put" race). They perform the check-and-act atomically, so you don't need an external lock around map updates. Reaching for these instead of locking a plain map is a strong signal you know the library.
Atomics - lock-free single-variable updates
The java.util.concurrent.atomic package gives you variables you can update atomically without locks, using hardware compare-and-swap underneath. This is the right tool for counters and flags that multiple threads modify - the fix for the volatile count++ problem from the JMM lesson.
AtomicInteger,AtomicLong- atomic counters:incrementAndGet(),compareAndSet(expected, new).AtomicReference<V>- atomic updates to an object reference.LongAdder- a counter optimized for high contention; spreads updates across cells and sums on read, outperformingAtomicLongwhen many threads increment hot.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic ++, no lock, no race
// Lock-free conditional update
counter.compareAndSet(5, 6); // set to 6 only if currently 5
Note: Atomics are your lock-free building block. For a simple shared counter,
AtomicIntegeris both correct and faster than a lock. Under heavy contention from many threads,LongAdderis faster still. We go deep on the compare-and-swap mechanism behind these in the lock-free module - here, just know that "shared counter" should make you thinkAtomicInteger/LongAdder, notsynchronized.
Synchronizers - coordinating threads
These coordinate when threads proceed, rather than guarding shared data. Each solves a specific coordination shape.
CountDownLatch- one or more threads wait until a count reaches zero. One-shot: "wait until N things have happened, then proceed." (e.g., wait for 5 services to initialize before starting.)CyclicBarrier- a set of threads wait for each other at a barrier point, then all proceed together; reusable across rounds (unlike the one-shot latch).Semaphore- maintains a set of permits; threads acquire and release them to limit how many run a section concurrently. The tool for rate-limiting or capping concurrent access to a resource.Phaser- a flexible, multi-phase barrier for more dynamic coordination where the number of parties can change.
CountDownLatch ready = new CountDownLatch(3);
// worker threads call ready.countDown() when done initializing
ready.await(); // main thread blocks until all 3 have counted down
Semaphore permits = new Semaphore(10); // allow 10 concurrent users
permits.acquire();
try { /* limited-capacity section */ } finally { permits.release(); }
Interview: A frequent disambiguation:
CountDownLatchvsCyclicBarrier? A latch is one-shot and asymmetric - threads count down and waiters proceed when it hits zero; it can't be reset. A barrier is reusable and symmetric - all parties wait for each other and release together, then the barrier resets for the next round. Mnemonic: latch = "wait for N events to finish" (one time); barrier = "N threads rendezvous repeatedly." Naming that distinction cleanly is a reliable signal you know the synchronizers.
Locks - explicit locking (recap)
The locks subpackage, covered in the previous lesson: ReentrantLock (explicit, feature-rich exclusive lock), ReadWriteLock/ReentrantReadWriteLock (many readers or one writer), and StampedLock (adds optimistic reads). Reach here when synchronized lacks a feature you need.
Futures - results from async work
Future<V> represents a result that will be available later (from executor.submit). CompletableFuture<V> is the far more powerful async type - it lets you chain, combine, and compose asynchronous operations without blocking, and it gets its own full lesson next.
Putting pieces together
The tools compose. Here's a small, realistic snippet using several j.u.c categories at once - a pool, a concurrent map, an atomic, and a latch - to show how they fit:
ExecutorService pool = Executors.newFixedThreadPool(4);
ConcurrentHashMap<String, Integer> wordCounts = new ConcurrentHashMap<>();
AtomicInteger processed = new AtomicInteger();
CountDownLatch done = new CountDownLatch(documents.size());
for (String doc : documents) {
pool.submit(() -> {
for (String word : doc.split("\\s+")) {
wordCounts.merge(word, 1, Integer::sum); // atomic per-key update
}
processed.incrementAndGet(); // lock-free counter
done.countDown(); // signal this doc is done
});
}
done.await(); // block until every document is processed
pool.shutdown();
System.out.println("Processed " + processed.get() + " docs, "
+ wordCounts.size() + " unique words");
No explicit locks anywhere, yet it's fully thread-safe: ConcurrentHashMap.merge handles per-key updates atomically, AtomicInteger handles the counter, the pool manages threads, and the latch coordinates "wait until all done." That's the j.u.c philosophy - assemble correct concurrency from well-tested parts rather than hand-rolling locks. This exact shape (pool + concurrent map + atomic + latch) is a strong template for "design a concurrent X" interview questions.
How to use this tour
You don't memorize every class - you build a mental index so that when a problem appears, the right tool comes to mind:
| When you need... | Reach for |
|---|---|
| A thread-safe map | ConcurrentHashMap |
| A read-heavy, write-rare list | CopyOnWriteArrayList |
| Producer-consumer handoff | a BlockingQueue |
| A shared counter | AtomicInteger / LongAdder |
| To wait for N tasks to finish | CountDownLatch |
| Threads to rendezvous repeatedly | CyclicBarrier |
| To cap concurrent access to a resource | Semaphore |
| To run tasks on a pool | ExecutorService |
| To compose async operations | CompletableFuture |
Interview: The meta-signal interviewers look for: do you reach for the library or reinvent it? When asked to design something concurrent, mentioning the right j.u.c class ("I'd back the producer-consumer with a
BlockingQueue," "I'd use aConcurrentHashMapwithcomputeIfAbsent," "I'd cap concurrency with aSemaphore") signals real-world experience. Hand-rolling a thread-safe queue withwait/notifywhenBlockingQueueexists is a minor red flag - it suggests you haven't worked with the standard tools. Know the catalog; use the catalog.
That's the toolbox. The last two Java lessons cover the two newest, most powerful members in depth: virtual threads (Java 21's lightweight threads) and CompletableFuture (composable async). Those are next.