13.4 Concurrent Collections, Atomic Variables, Explicit Locks, and `CompletableFuture`

Key Takeaways

  • Atomic variables in java.util.concurrent.atomic employ hardware-level Compare-And-Swap (CAS) instructions to provide thread-safe, lock-free mutations.

  • ConcurrentHashMap provides high-concurrency read and write operations via bucket-level locking without throwing ConcurrentModificationException, unlike synchronized map wrappers that lock the entire table.

  • CopyOnWriteArrayList creates a new internal copy of the backing array on every write, providing immutable snapshot iterators ideal for read-heavy, low-mutation scenarios.

  • ReentrantLock provides explicit, interruptible, timed, and fair lock acquisition capabilities that exceed intrinsic monitors, requiring mandatory lock() outside and unlock() inside a finally block.

  • CompletableFuture enables reactive, non-blocking asynchronous pipelines, supporting transformation (thenApply), composition (thenCompose), combination (thenCombine), and robust fallback error handling (exceptionally, handle).

Last updated: October 2026

Concurrent Collections, Atomic Variables, Explicit Locks, and CompletableFuture

While intrinsic synchronization (synchronized) provides simple mutual exclusion, high-throughput applications require specialized concurrent primitives to minimize thread contention and maximize multi-core hardware saturation. Java SE 17 provides lock-free atomic variables, highly optimized concurrent collections, explicit lock abstractions, and composable asynchronous completion stages.


Lock-Free Atomic Variables and Compare-And-Swap (CAS)

The java.util.concurrent.atomic package provides classes such as AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference<V>. These classes support thread-safe operations on single variables without utilizing intrinsic locks.

Compare-And-Swap (CAS) Mechanics

Instead of blocking threads, atomic classes utilize CPU hardware instructions known as Compare-And-Swap (e.g., cmpxchg on x86 architectures). A CAS instruction operates on three operands:

  1. A memory location (VV).
  2. The expected old value (AA).
  3. The new value to write (BB).

The processor atomically updates VV to BB if and only if the current value at VV matches AA. If the value has changed in the interim, the operation fails, and the calling thread loops to retry the operation (optimistic retry loop):

AtomicInteger counter = new AtomicInteger(0);

// Atomic increment:
int nextVal = counter.incrementAndGet(); // Returns 1

// Conditional CAS update:
boolean updated = counter.compareAndSet(1, 10); // Sets to 10 if current is 1

// Atomic functional transformation:
int transformed = counter.updateAndGet(x -> x * 2); // 10 * 2 = 20

CAS eliminates thread suspension, context-switch overhead, and priority inversion, providing significantly higher throughput under low-to-moderate contention than intrinsic locks.

Concurrent Collections vs. Synchronized Wrappers

Legacy collections can be synchronized using factory methods such as Collections.synchronizedMap() or Collections.synchronizedList().

The Problem with Synchronized Wrappers

  • Single Mutex Bottleneck: Synchronized wrappers wrap the underlying collection with a single, coarse-grained mutex lock. Every read and write operation contends for the same lock, serializing all access.
  • Unsafe Iteration: Iterators over synchronized wrappers are not thread-safe unless the client code manually synchronizes on the collection wrapper during the entire iteration loop. Failure to do so risks ConcurrentModificationException.
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// MUST manually synchronize on syncMap during iteration:
synchronized (syncMap) {
    for (String key : syncMap.keySet()) { /* ... */ }
}

High-Performance Concurrent Collections

Java provides modern concurrent collections designed from the ground up for multi-threaded workloads:

CollectionConcurrency MechanismIteration BehaviorIdeal Use Case
ConcurrentHashMapBucket-level locks and CAS operationsWeakly consistent; never throws ConcurrentModificationExceptionGeneral-purpose, high-concurrency key-value store
CopyOnWriteArrayListClones backing array on every mutationSnapshot view; cannot modify during iterationRead-heavy, rarely modified lists (e.g., event listeners)
ConcurrentLinkedQueueLock-free queue using CAS pointersWeakly consistentHigh-throughput, non-blocking FIFO queue
ArrayBlockingQueueBounded array with explicit locksWeakly consistentFixed-capacity producer-consumer pipelines
LinkedBlockingQueueOptionally bounded linked nodesWeakly consistentVariable-capacity producer-consumer pipelines

Atomic Operations in ConcurrentHashMap

ConcurrentHashMap provides atomic compound methods that avoid external synchronization:

  • putIfAbsent(K, V): Inserts the value only if the key is not currently mapped.
  • computeIfAbsent(K, Function): Computes and inserts a value atomically if the key is absent.
  • computeIfPresent(K, BiFunction): Recomputes a mapping if the key is present.
  • merge(K, V, BiFunction): Merges an existing value with a new value atomically.

CopyOnWriteArrayList Semantics

In a CopyOnWriteArrayList, all mutative operations (add, set, remove) create a fresh copy of the internal backing array. Readers iterate over an immutable snapshot taken when the iterator was constructed. Iterators over a CopyOnWriteArrayList do not support remove(); calling iterator.remove() throws UnsupportedOperationException.


Explicit Locks: ReentrantLock and ReentrantReadWriteLock

The java.util.concurrent.locks package provides explicit locking interfaces that offer advanced capabilities beyond intrinsic monitor blocks.

ReentrantLock Standard Idiom

Unlike synchronized blocks, explicit locks require manual acquisition and release. The call to lock() must occur immediately before the try block, and unlock() must reside in the finally block:

Lock lock = new ReentrantLock();
lock.lock(); // Acquire BEFORE try!
try {
    // Critical section
} finally {
    lock.unlock(); // Ensure lock is ALWAYS released
}

Capabilities Beyond Intrinsic Locks

  1. Non-Blocking Lock Acquisition: lock.tryLock() attempts to acquire the lock immediately without waiting, returning true if successful and false otherwise.
  2. Timed Lock Acquisition: lock.tryLock(500, TimeUnit.MILLISECONDS) waits up to the timeout before giving up, breaking deadlock hazards.
  3. Interruptible Acquisition: lock.lockInterruptibly() acquires the lock unless the current thread is interrupted, responding promptly to cancellation.
  4. Fairness Policies: new ReentrantLock(true) creates a fair lock that grants access in FIFO order to the longest-waiting thread, eliminating thread starvation at the expense of throughput.

ReentrantReadWriteLock

Optimizes scenarios where read operations substantially outnumber write operations:

  • Read Lock (lock.readLock()): Multiple threads can hold the read lock simultaneously, provided no thread holds the write lock.
  • Write Lock (lock.writeLock()): Mutually exclusive. Only one thread can hold the write lock, barring all other readers and writers.
  • Lock Downgrading vs. Upgrading: Holding the write lock and acquiring the read lock before releasing the write lock (downgrading) is fully supported. However, acquiring the write lock while holding a read lock (upgrading) is not supported and triggers deadlock.

Asynchronous Orchestration with CompletableFuture

Introduced in Java 8 and enhanced in Java 9 through 17, CompletableFuture<T> implements both Future<T> and CompletionStage<T>. It enables non-blocking, declarative, event-driven pipelines.

[supplyAsync] ---> .thenApply() ---> .thenCombine() ---> .exceptionally()
(Fetch Data)       (Transform)        (Join Another)      (Fallback)

1. Initiating Asynchronous Pipelines

  • CompletableFuture.supplyAsync(Supplier<U>): Executes the supplier asynchronously on ForkJoinPool.commonPool() and returns CompletableFuture<U>.
  • CompletableFuture.runAsync(Runnable): Executes a Runnable asynchronously, returning CompletableFuture<Void>.
  • Both methods offer overloads accepting a custom Executor.

2. Chaining Stages (Transformations & Callbacks)

  • thenApply(Function<T, R>): Synchronously transforms the result of the previous stage into a new type R.
  • thenAccept(Consumer<T>): Consumes the output of the stage, returning CompletableFuture<Void>.
  • thenRun(Runnable): Runs an action after the prior stage completes without consuming its output.
  • Async Variants (thenApplyAsync, etc.): Execute the dependent stage on a different thread using ForkJoinPool.commonPool() or a designated Executor.

3. Composing and Combining Futures

  • thenCompose(Function<T, CompletableFuture<R>>): Flattens nested futures (analogous to flatMap), chaining dependent asynchronous calls.
  • thenCombine(CompletionStage<U>, BiFunction<T, U, V>): Runs two independent stages in parallel and combines their results with a BiFunction when both complete.
  • CompletableFuture.allOf(CompletableFuture<?>... cfs): Returns a future that completes when all input futures complete.
  • CompletableFuture.anyOf(CompletableFuture<?>... cfs): Returns a future that completes when any of the input futures completes.

4. Resilient Exception Handling

  • exceptionally(Function<Throwable, T>): Catches an unhandled exception thrown anywhere up the pipeline and returns a fallback recovery value.
  • handle(BiFunction<T, Throwable, R>): Always executes upon stage completion, receiving either the computed result or the thrown Throwable.
Test Your Knowledge

A developer iterates through a CopyOnWriteArrayList and attempts to remove specific elements:

List<String> list = new CopyOnWriteArrayList<>(List.of("Alpha", "Beta", "Gamma"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String item = it.next();
    if ("Beta".equals(item)) {
        it.remove();
    }
}

What is the result when this code executes?

A

The list successfully removes 'Beta', leaving ['Alpha', 'Gamma'].

B

A ConcurrentModificationException is thrown at runtime when it.next() is called after removal.

C

An UnsupportedOperationException is thrown at runtime on it.remove().

D

The code fails to compile because CopyOnWriteArrayList does not implement Iterable.

Test Your Knowledge

Consider a thread that acquires locks using a ReentrantReadWriteLock:

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

readLock.lock();
try {
    // Thread needs to perform an update
    writeLock.lock(); // ATTEMPT LOCK UPGRADE
    try {
        System.out.println("Updated successfully");
    } finally {
        writeLock.unlock();
    }
} finally {
    readLock.unlock();
}

What happens when a thread executes this sequence?

A

The thread deadlocks with itself because ReentrantReadWriteLock does not support lock upgrading.

B

The read lock is automatically released before acquiring the write lock and re-acquired afterward.

C

The thread executes normally and prints 'Updated successfully'.

D

An IllegalMonitorStateException is thrown on writeLock.lock().

Test Your Knowledge

Examine the following asynchronous pipeline constructed with CompletableFuture:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (true) throw new RuntimeException("Service Error");
    return "Success";
}).exceptionally(ex -> {
    return "Recovered";
}).thenApply(String::toUpperCase);

System.out.println(future.join());

What is printed to the console?

A

A CompletionException wrapping RuntimeException is thrown.

B

RECOVERED

C

Success

D

Service Error

Sections you finish are checked off in the contents.