Concurrency Utilities

A complete guide to java.util.concurrent: thread pools, Callable and Future, synchronization barriers, locks, atomic variables, concurrent collections, blocking queues, and CompletableFuture for asynchronous pipelines.

Why java.util.concurrent Exists

The raw thread primitives covered in the Multithreading tutorial, the Thread class, synchronized, and wait/notify, give you the building blocks. But building real concurrent systems on primitives alone is like building a house with only a chisel. Everything is possible in principle, and in practice you spend most of your effort on low-level plumbing that has already been solved.

Java 5 introduced java.util.concurrent, designed by Doug Lea and a team of concurrency experts. It provides thread pools so you stop creating and destroying threads for every task. It provides Future so you can retrieve results from concurrent computations. It provides synchronization barriers so groups of threads can coordinate at checkpoints. It provides locks with richer semantics than synchronized, atomic operations without any locking at all, thread-safe collections that do not serialize every access, and finally, with Java 8, a full pipeline API for asynchronous computation through CompletableFuture. These are the tools you use in production.

Executor, ExecutorService, and Thread Pools

Creating a new Thread object is expensive: it involves a system call to the operating system to allocate a stack and register the thread for scheduling. For work that involves many short tasks, creating a fresh thread for each one and destroying it when it finishes wastes far more time on thread management than on actual computation.

A thread pool solves this by maintaining a set of pre-created threads that wait for tasks. You submit a task to the pool, a free thread picks it up and executes it, and when finished, the thread returns to the pool ready for the next task. Thread creation overhead is paid once at startup rather than per task.

The Executor interface is the simplest abstraction: one method, execute(Runnable). ExecutorService extends it with lifecycle management ( shutdown(),awaitTermination()) and task submission that returns a Future (submit(Callable)). The Executors factory class provides common thread pool configurations.

Common Executors factory methods

These cover the most frequent use cases. For more control, construct a ThreadPoolExecutor directly with custom core size, max size, keep-alive time, and work queue.

  • newFixedThreadPool(n):Creates a pool with exactly n threads. Tasks beyond what the threads can handle queue up. Good for CPU-bound work where you want to match the thread count to available cores.
  • newCachedThreadPool():Creates threads on demand as tasks arrive and reuses idle threads. Threads that have been idle for 60 seconds are removed. Good for many short-lived I/O-bound tasks with variable arrival rates.
  • newSingleThreadExecutor():A pool of exactly one thread. Tasks run sequentially in submission order. Good for serializing access to a resource from multiple threads.
  • newScheduledThreadPool(n):Like FixedThreadPool but supports scheduled and periodic task execution. Returns a ScheduledExecutorService.
  • newWorkStealingPool():Creates a ForkJoinPool sized to available processors. Idle threads steal tasks from busy threads. Good for recursive divide-and-conquer work.

ExecutorService and Thread Pools

Java

Submitting tasks to fixed, cached, and single-thread pools, and properly shutting down the executor.

Callable and Future

Runnable cannot return a value or throw a checked exception. For concurrent tasks that produce a result, Callable<T> fills that gap. Its single method, T call() throws Exception, can return any type and declare any checked exception.

Submitting a Callable to an ExecutorService via submit() immediately returns a Future<T>. The Future is a handle to the eventual result. Calling future.get() blocks the calling thread until the task completes and then returns the result. If the task threw an exception, get() wraps it in an ExecutionException. A timed version, get(timeout, unit), throws TimeoutException if the result is not ready in time, which prevents the caller from waiting indefinitely for a slow or hung task.

Submitting multiple Callable tasks at once and collecting the futures into a list is the standard pattern for fan-out work: dispatch multiple independent computations in parallel, then gather their results in order. The loop that calls get() on each future does block per task, but because the tasks run concurrently, the total wall-clock time is roughly the duration of the slowest task rather than the sum of all tasks.

Callable, Future, and invokeAll()

Java

Submitting parallel computation tasks, collecting futures, handling exceptions, and using invokeAll for batch submission.

ScheduledExecutorService

ScheduledExecutorService extends ExecutorService with the ability to schedule tasks to run after a delay or at a fixed rate. It replaces the legacy Timer class, which ran all scheduled tasks on a single thread, meaning a slow task could delay all subsequent ones. With ScheduledExecutorService you configure the pool size and tasks are dispatched to available threads.

The distinction between scheduleAtFixedRate and scheduleWithFixedDelay is important in practice. A fixed-rate schedule tries to run the task every N milliseconds regardless of how long the task took. If the task takes longer than the period, the next execution starts immediately after the current one finishes. A fixed-delay schedule waits N milliseconds after the previous execution finished before starting the next one. For tasks where the interval between completions matters more than the interval between starts, fixed-delay is the safer choice.

ScheduledExecutorService

Java

Scheduling a one-shot delayed task, a fixed-rate heartbeat, and a fixed-delay poller.

Synchronization Aids: CountDownLatch, CyclicBarrier, and Semaphore

These three classes solve coordination problems that would require significant manual coding with raw wait/notify. Each one is designed for a specific pattern.

CountDownLatch

A CountDownLatch is initialized with a count. Any thread that calls await() blocks until the count reaches zero. Other threads decrement the count with countDown(). When the count reaches zero, all waiting threads are released simultaneously and the latch is permanently open. It cannot be reset. The canonical use case is a starting gate: one thread waits for N workers to signal readiness, then processes all their results. It is also used as a starting pistol: N threads all await a signal from one coordinator.

CyclicBarrier

A CyclicBarrier waits until a fixed number of threads (the "parties") all call await(). Once all parties arrive, an optional barrier action runs, and then all threads are released to continue. The key difference from CountDownLatch is that a CyclicBarrier resets automatically after every trip and can be reused in a loop. This makes it the right tool for iterative parallel algorithms where threads synchronize at the end of each phase before moving to the next.

Semaphore

A Semaphore maintains a set of permits. Calling acquire() blocks the thread if no permit is available and proceeds once one is released. Calling release() returns a permit. A semaphore with one permit behaves like a mutex. A semaphore with N permits enforces that at most N threads access a resource simultaneously. Classic uses include limiting the number of concurrent database connections, concurrent HTTP requests to an external API, or concurrent file reads from a slow disk.

CountDownLatch, CyclicBarrier, and Semaphore

Java

Coordinating worker startup with a latch, phased execution with a cyclicbarrier, and connection pool limiting with a semaphore.

ReentrantLock, ReadWriteLock, and Condition

The synchronized keyword works well for most situations, but it has limitations. You cannot try to acquire a lock without blocking. You cannot give up waiting after a certain time. You cannot interrupt a thread that is waiting for a monitor. And a synchronized block can only have one wait/notify condition queue. ReentrantLock removes all of these restrictions.

It is called "reentrant" because a thread that already holds the lock can acquire it again without blocking. This is the same behaviour as synchronized, which also allows a thread to re-enter a synchronized method it already holds the lock for. The lock keeps an internal hold count and releases only when the count returns to zero.

The critical usage rule for ReentrantLock is that unlock must always be called in a finally block. synchronized releases the lock automatically when the block exits, even on exception.ReentrantLock does not. A lock that is never released because an exception skipped the unlock call will deadlock every thread that tries to acquire it later.

ReadWriteLock provides two views of the same lock: a shared read lock and an exclusive write lock. Multiple threads can hold the read lock simultaneously as long as no thread holds the write lock. This is ideal for data structures that are read far more often than they are written: the majority of operations (reads) proceed in parallel rather than queuing behind a single exclusive lock.

ReentrantLock, tryLock(), ReadWriteLock, and Condition

Java

Timed lock acquisition, non-blocking tryLock(), a read-heavy cache protected by ReadWriteLock, and Condition-based waiting.

Atomic Variables

The classes in java.util.concurrent.atomic provide thread-safe operations on single variables without any locking. They achieve this using CPU-level compare-and-swap (CAS) instructions: the hardware atomically reads a value, compares it to an expected value, and writes a new value only if the comparison succeeded. If another thread changed the value in the meantime, the operation detects the mismatch and retries. This is called optimistic locking: it assumes conflicts are rare and handles them when they occur, rather than preventing them with a mutex.

For simple operations like incrementing a counter, checking a flag, or updating a reference, atomic variables are faster than synchronized blocks because they avoid the overhead of acquiring and releasing a monitor lock. Under high contention (many threads competing for the same variable), CAS can spin more than a lock would, but for typical workloads the non-blocking approach wins.

The key classes are AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference<T>. Java 8 added LongAdder and LongAccumulator for high-throughput counters with many concurrent updaters, trading slightly higher read cost for dramatically better write throughput.

AtomicInteger, AtomicLong, and AtomicReference

Java

Thread-safe counter, compare-and-swap for optimistic updates, and atomic reference swapping.

Concurrent Collections: ConcurrentHashMap and CopyOnWriteArrayList

You might think you can make a collection thread-safe by wrapping it with Collections.synchronizedMap(). This does prevent data corruption, but it does so by locking the entire map for every single operation. Two threads cannot read from different parts of the map at the same time. Under any meaningful load, the lock becomes a bottleneck.

ConcurrentHashMap uses a fundamentally different strategy. It divides the map into segments (in Java 8+ it uses per-bucket CAS and fine-grained synchronization) so that multiple threads can read and write different segments simultaneously. In practice, reads are almost never blocked. Writes to different buckets proceed in parallel. Only writes to the same bucket contend. For most workloads this is dramatically faster than a synchronized map.

CopyOnWriteArrayList takes the opposite approach: every write (add, set, remove) creates a fresh copy of the underlying array. Reads always see a consistent snapshot of the array at the moment the read began, with no locking at all. This makes writes expensive but reads genuinely zero-cost from a synchronization standpoint. The trade-off makes sense when reads vastly outnumber writes, such as a list of event listeners or a configuration list that is read thousands of times per second and changed once per minute.

ConcurrentHashMap and CopyOnWriteArrayList

Java

Atomic map operations, concurrent updates from multiple threads, and the iteration safety of CopyOnWriteArrayList.

BlockingQueue

BlockingQueue is the standard building block for producer-consumer pipelines in Java. Unlike a regular queue, its put() method blocks when the queue is full, and its take() method blocks when the queue is empty. This turns the coordination problem that required careful wait/notify code into two simple method calls. All of the synchronization is handled inside the queue implementation.

ArrayBlockingQueue is backed by a fixed-size array. The capacity is set at construction time and cannot grow. This gives it a predictable memory footprint and makes it the right choice when you want to apply back-pressure: if producers consistently outpace consumers, they will be forced to slow down. LinkedBlockingQueue is backed by a linked list. It can optionally have a capacity limit; without one it is unbounded. Unbounded queues let producers run ahead of consumers as far as memory allows, which may not be desirable but can prevent pipeline stalls in bursty workloads.

BlockingQueue: Producer-Consumer Pipeline

Java

Multiple producers feeding into a bounded ArrayBlockingQueue, multiple consumers draining it, coordinated with a poison pill shutdown.

CompletableFuture (Java 8+)

The plain Future interface has a significant limitation: the only way to get a result from it is to call get(), which blocks the calling thread. You cannot attach a callback that fires when the result is ready, you cannot compose two futures so that the second starts when the first completes, and you cannot chain transformations without blocking at each step.

CompletableFuture<T> implements Future<T> and adds a full pipeline API. You can transform the result with thenApply(), consume it with thenAccept(), chain a follow-up computation with thenCompose(), combine two independent futures with thenCombine(), handle errors with exceptionally(), and wait for the fastest of several futures with anyOf(). All of this composes without blocking the calling thread. Callbacks run on the common ForkJoinPool or on an executor you specify.

CompletableFuture method categories

Methods following the 'thenApplyAsync' naming convention run the callback on a separate thread pool rather than the completing thread.

  • supplyAsync(Supplier) / runAsync(Runnable):Start an asynchronous computation and return a CompletableFuture.
  • thenApply(fn):Transform the result. Returns CompletableFuture where U is the return type of fn.
  • thenAccept(consumer):Consume the result without producing a new value. Returns CompletableFuture.
  • thenRun(runnable):Run an action after completion that neither reads the result nor returns a value.
  • thenCompose(fn):Chain a dependent async operation. fn must return a CompletableFuture. Prevents nested CompletableFuture>.
  • thenCombine(other, fn):Wait for two independent futures and combine their results with a BiFunction.
  • exceptionally(fn):Recover from an exception by computing a fallback value.
  • allOf(futures) / anyOf(futures):Wait for all futures to complete, or return as soon as any one of them completes.

CompletableFuture Pipelines

Java

Async supply, transform, combine, error recovery, and allOf for waiting on multiple parallel operations.

Quiz - Test Your Knowledge

Ten questions covering thread pools and ExecutorService, Callable and Future, CountDownLatch vs. CyclicBarrier, Semaphore, ReentrantLock, atomic variables, ConcurrentHashMap, BlockingQueue, and CompletableFuture. Read each option carefully before selecting your answer.

Knowledge Check

1. What is the key difference between Callable<T> and Runnable?

2. You submit a Callable to an ExecutorService and call future.get(). What happens if the Callable threw an exception during execution?

3. What does CountDownLatch.await() do?

4. What is the difference between CountDownLatch and CyclicBarrier?

5. What does a Semaphore with 3 permits enforce?

6. What advantage does ReentrantLock have over the synchronized keyword?

7. Why is AtomicInteger preferred over a synchronized int counter for simple increment operations?

8. What makes ConcurrentHashMap thread-safe without locking the entire map for every operation?

9. What does CompletableFuture.thenApply(fn) do?

10. What is the difference between shutdownNow() and shutdown() on an ExecutorService?