9.4 Concurrent Collections, Atomics, and Explicit Locks

Key Takeaways

  • ConcurrentHashMap provides high-throughput thread safety using bucket-level fine-grained locking and CAS, strictly forbids null keys and values, and yields weakly consistent iterators that never throw ConcurrentModificationException.
  • CopyOnWriteArrayList creates a fresh backing array upon every mutation, providing immutable snapshot iterators whose remove() method throws UnsupportedOperationException.
  • Atomic classes in java.util.concurrent.atomic leverage low-level Compare-And-Swap (CAS) CPU instructions to perform lock-free, thread-safe mutations on single variables.
  • ReentrantLock offers explicit locking with tryLock() timeouts, condition variables, and fairness options, while ReentrantReadWriteLock permits multiple concurrent readers but only a single exclusive writer.
  • ReentrantReadWriteLock supports lock downgrading from write lock to read lock, but attempting to upgrade a read lock to a write lock causes a deadlock.
Last updated: September 2026

Concurrent Collections, Atomics, and Explicit Locks

High-performance concurrent applications require specialized data structures and synchronization primitives that avoid coarse-grained monitor bottlenecks. The java.util.concurrent package provides lock-free collections, copy-on-write collections, atomic variable wrappers, explicit locks, and thread coordination synchronizers.


1. Concurrent Collections vs. Synchronized Wrappers

Legacy synchronized collections (Collections.synchronizedMap(), Vector, Hashtable) synchronize every operation on a single, coarse-grained object monitor. This serializes all operations, creating severe contention under high concurrency, and still requires external synchronization when iterating.

In contrast, modern concurrent collections use lock striping, Compare-And-Swap (CAS) operations, and copy-on-write arrays to enable concurrent reads and writes without global locks.


2. Core Concurrent Collection Implementations

A. ConcurrentHashMap<K, V>

  • Lock Granularity: Employs fine-grained locking on individual hash bucket bins using synchronized blocks and CAS operations. Reads are entirely non-blocking.
  • Null Policy: Strictly forbids null keys and null values. Attempting to insert a null key or null value throws a NullPointerException! (Unlike standard HashMap which permits one null key and multiple null values).
  • Atomic Compound Operations:
    • putIfAbsent(key, value): Inserts value only if key is not already mapped.
    • computeIfAbsent(key, mappingFunction): Computes and inserts value atomically if key is absent.
    • computeIfPresent(key, remappingFunction): Updates value atomically if key is present.
    • merge(key, value, remappingFunction): Combines old and new values atomically.
  • Weakly Consistent Iterators: Iterators reflect the map state at or after iterator creation, will never throw ConcurrentModificationException, and can tolerate concurrent modifications.
ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
// map.put(null, 1);    // THROWS NullPointerException!
// map.put("KEY", null); // THROWS NullPointerException!

// Thread-safe atomic counter increment
map.merge("visitors", 1, Integer::sum);

B. ConcurrentSkipListMap<K, V> and ConcurrentSkipListSet<E>

  • Structure: A thread-safe, scalable concurrent sorted map based on a probabilistic SkipList data structure.
  • Ordering: Maintains elements in natural key order or according to a provided Comparator.
  • Performance: Provides $O(\log n)$ time cost for containsKey, get, put, and remove.
  • Null Policy: Strictly forbids null keys and null values (throws NullPointerException).

C. CopyOnWriteArrayList<E> and CopyOnWriteArraySet<E>

  • Design: Every mutative operation (add(), set(), remove()) allocates a brand new copy of the underlying backing array.
  • Workload Fit: Highly optimized for read-heavy, write-rare scenarios (e.g., event listener lists, routing tables).
  • Snapshot Iterators:
    • Iterators operate on an immutable snapshot of the backing array captured at the instant the iterator was created.
    • Concurrent modifications made to the list after iterator creation do not affect the iteration snapshot.
    • ConcurrentModificationException is never thrown.
    • Iterator Mutations Forbidden: Invoking iterator.remove(), iterator.add(), or iterator.set() throws UnsupportedOperationException!
List<String> list = new CopyOnWriteArrayList<>(List.of("Alpha", "Beta"));
for (String item : list) {
    list.add("Gamma"); // Modifies list, but does not affect the active iterator snapshot
    // iterator.remove(); // THROWS UnsupportedOperationException!
}
System.out.println(list); // Prints: [Alpha, Beta, Gamma, Gamma]

D. BlockingQueue<E> Implementations & Operations Matrix

BlockingQueue implementations coordinate producer-consumer processing pipelines by blocking threads when attempting to insert into a full queue or retrieve from an empty queue.

Queue ActionThrows ExceptionSpecial ValueBlocks IndefinitelyBlocks with Timeout
Insertadd(e) (throws IllegalStateException)offer(e) (returns false)put(e)offer(e, timeout, unit)
Removeremove() (throws NoSuchElementException)poll() (returns null)take()poll(timeout, unit)
Examineelement() (throws NoSuchElementException)peek() (returns null)N/AN/A

Common Implementations:

  • ArrayBlockingQueue: Bounded array-backed FIFO queue.
  • LinkedBlockingQueue: Optionally bounded node-linked FIFO queue.
  • PriorityBlockingQueue: Unbounded priority heap queue.
  • SynchronousQueue: Zero-capacity direct handoff queue (each insert must wait for a concurrent take).

3. Atomic Variables and Compare-And-Swap (CAS)

The java.util.concurrent.atomic package (AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference<V>) provides lock-free, thread-safe single-variable mutations.

Hardware Compare-And-Swap (CAS)

CAS is a low-level CPU instruction that performs an optimistic read-modify-write: CAS(V,expectedValue,newValue)\text{CAS}(V, \text{expectedValue}, \text{newValue}) If memory location $V$ still contains $\text{expectedValue}$, it updates $V$ to $\text{newValue}$ and returns true. If another thread modified $V$ in the interim, CAS returns false and the operation loops (spins) and retries without suspending the thread.

AtomicInteger counter = new AtomicInteger(0);

int next = counter.incrementAndGet(); // Atomic ++counter
int prev = counter.getAndIncrement(); // Atomic counter++

// Atomic conditional update
boolean success = counter.compareAndSet(2, 10); // If value is 2, sets to 10

// Atomic functional updates
counter.updateAndGet(x -> x * 2);
counter.accumulateAndGet(5, (x, y) -> x + y);

AtomicReference<V>

Used for atomic updates on custom immutable domain objects:

record UserConfig(String theme, int fontSize) {}
AtomicReference<UserConfig> configRef = new AtomicReference<>(new UserConfig("DARK", 14));
configRef.updateAndGet(current -> new UserConfig("LIGHT", current.fontSize()));

High-Contention Counters: LongAdder vs. AtomicLong

Under heavy multi-thread write contention, threads competing on a single AtomicLong spend excessive CPU cycles spinning on CAS retries. LongAdder distributes updates across an internal array of striped cell counters and sums them on sum(), eliminating contention bottlenecks.


4. Explicit Locks (java.util.concurrent.locks)

ReentrantLock

ReentrantLock implements the Lock interface, providing capabilities beyond intrinsic synchronized monitors:

Lock lock = new ReentrantLock(true); // 'true' enables FIFO fair acquisition ordering

lock.lock(); // Blocks until lock is acquired
try {
    // Critical Section
} finally {
    lock.unlock(); // ALWAYS unlock in a finally block!
}

Advanced ReentrantLock Capabilities:

  1. Non-Blocking Lock Acquisition (tryLock()): if (lock.tryLock()) { try { ... } finally { lock.unlock(); } } prevents deadlocks.
  2. Timed Lock Acquisition (tryLock(timeout, unit)): Waits for lock up to a specified deadline.
  3. Interruptible Acquisition (lockInterruptibly()): Responds immediately to Thread.interrupt() while waiting.
  4. Condition Variables (lock.newCondition()): Replaces wait()/notify() with multiple independent wait-sets per lock (await(), signal(), signalAll()).

ReentrantReadWriteLock

Maintains a pair of associated locks: a shared read lock and an exclusive write lock:

  • Multiple threads can hold the read lock simultaneously when no write lock is held.
  • Only one thread can hold the write lock (exclusive).
  • Lock Downgrading IS Permitted: Hold write lock $ ightarrow$ acquire read lock $ ightarrow$ release write lock.
  • Lock Upgrading is NOT Permitted: Holding a read lock and attempting to acquire a write lock causes a permanent deadlock!
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock rLock = rwLock.readLock();
Lock wLock = rwLock.writeLock();

// Lock Downgrading Example (Legal)
wLock.lock();
try {
    // Modify shared state...
    rLock.lock(); // Acquire read lock while holding write lock (Downgrade)
} finally {
    wLock.unlock(); // Release write lock; still hold read lock
}
try {
    // Read shared state...
} finally {
    rLock.unlock();
}

5. Thread Coordination Synchronizers

SynchronizerOperational ModelReusability
CountDownLatch(int count)Blocks threads at await() until countDown() is called $N$ times.One-time use (count cannot be reset)
CyclicBarrier(int parties, Runnable action)Blocks $N$ threads until all $N$ arrive at await(). Executes barrier action, then releases all threads.Reusable (automatically resets for next cycle)
Semaphore(int permits)Restricts access to a finite pool of $N$ shared resources via acquire() and release().Reusable permits pool

CountDownLatch Example (One-Way Gate)

CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        System.out.println("Service initialized");
        latch.countDown(); // Decrements count by 1
    }).start();
}

latch.await(); // Main thread blocks until count reaches 0
System.out.println("All services initialized. System startup complete.");

CyclicBarrier Example (Multi-Phase Synchronization)

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("=== Phase Completed ==="));

Runnable worker = () -> {
    try {
        System.out.println("Phase 1 computation");
        barrier.await(); // Waits for all 3 threads, triggers action, resets barrier
        System.out.println("Phase 2 computation");
        barrier.await(); // Reusable for phase 2!
    } catch (Exception e) {}
};
Loading diagram...
CountDownLatch vs. CyclicBarrier Synchronization Models
Test Your Knowledge

A developer iterates through a CopyOnWriteArrayList<String> containing elements ["Alpha", "Beta", "Gamma"] using its iterator. During iteration, the loop calls iterator.remove(). What is the result?

A
B
C
D
Test Your Knowledge

Which of the following operations on a ConcurrentHashMap instance will throw a NullPointerException?

A
B
C
D
Test Your Knowledge

Which of the following statements accurately contrasts CountDownLatch with CyclicBarrier in Java?

A
B
C
D
Test Your Knowledge

A thread holds a read lock on a ReentrantReadWriteLock and attempts to acquire the associated write lock on the same thread without releasing the read lock first. What occurs?

A
B
C
D