Home/Blog/Java/How It Works: Thread Safety Internals of Java's ConcurrentHashMap
JavaIntermediateβ€’9 min readβ€’

How It Works: Thread Safety Internals of Java's ConcurrentHashMap

Explore volatile reads, Compare-And-Swap (CAS) loops, bucket-head locking, and parallel resizing in Java's map.

Abstract Algorithms

Abstract Algorithms

Helping engineers master software engineering topics.

TLDR: Java's ConcurrentHashMap achieves high read throughput and thread-safe writes by avoiding table-level locks. It utilizes lock-free volatile node reads and isolates write locking to individual bucket heads, allowing separate threads to write to different slots concurrently.


πŸ“– Design Challenge: Thread-Safety without Blocking Reads

Imagine you are building a high-volume request routing registry for an API gateway. The gateway must map thousands of incoming HTTP routing keys to downstream service endpoints. Because routes change dynamically, this mapping is stored in a shared collection in memory.

A naive approach to thread-safety involves wrapping a standard HashMap in a synchronized wrapper:

// VIOLATION: Global locking on all read/write paths, creating massive thread contention
Map<String, String> routes = Collections.synchronizedMap(new HashMap<>());

While this code is thread-safe, it is highly unscalable under load. The synchronized wrapper uses a single global lock. If Thread A is reading Route 1, and Thread B wants to read Route 2, Thread B must wait.

Under heavy traffic (e.g., 50,000 requests per second), threads block each other continuously. The CPU spend more time context-switching threads than processing routes, degrading throughput.

To solve this, we need a map implementation that allows concurrent lock-free reads and parallel writes to different parts of the structure, ensuring that threads only block when attempting to write to the exact same hash bucket.


πŸ” Core Architecture: The Hash Bucket Array Structure

Java's ConcurrentHashMap (since Java 8) is organized as an array of bin nodes (buckets).

The architecture is composed of:

  1. Volatile Table Array: An array of Node<K,V> references marked as volatile to guarantee memory visibility across threads.
  2. Bucket Heads: The first node in each bin list. During write operations, locking is applied strictly to this head node.
  3. Bin Lists & Red-Black Trees: When multiple keys hash to the same bucket (collision), they form a linked list. If the list length exceeds a threshold (8 elements), it is converted into a balanced Red-Black tree (TreeNode) to keep lookup speeds at $O(\log N)$.
  4. Forwarding Nodes: Special nodes (ForwardingNode) inserted during table resizing to redirect readers to the new table.

The diagram below shows this bucket array structure:

graph TD
    Table[Node Array - Volatile] -->|Bin 0| Node1[Node 1: Key A]
    Table -->|Bin 1| Fwd[ForwardingNode]
    Table -->|Bin 2| TreeHead[TreeBin: Red-Black Tree]
    Node1 -->|Next| Node2[Node 2: Key B]
    Fwd -->|Points to| NewTable[New Node Array in RAM]

This node structure diagram illustrates how the ConcurrentHashMap separates bins. Bin 0 contains a standard linked list. Bin 1 holds a ForwardingNode indicating that the bin is currently being transferred to the new table. Bin 2 has been converted to a Red-Black tree (TreeBin) to handle high collision density, optimizing search speeds.


βš™οΈ Core Mechanics: CAS Loops, Volatile Nodes, and Synchronized Buckets

The ConcurrentHashMap coordinates concurrent updates using lock-free CPU instructions and target locking.

1. Lock-Free Reads

When a thread calls get(key), it computes the hash and locates the target bucket head. Since the node array and the next pointers within nodes are declared as volatile, the JVM guarantees that changes made by writer threads are visible to reader threads immediately without locks.

2. Compare-And-Swap (CAS) for Empty Buckets

If a thread attempts to insert a key into an empty bucket, it does not acquire a lock. Instead, it uses a CAS loop (using Unsafe or VarHandle memory operations). The thread checks if the bucket head pointer is null, and if so, atomically updates it with the new node. If another thread inserts a node at the same instant, the CAS fails, and the loop retries.

3. Bucket-Head Synchronization

If the target bucket is already occupied, the thread locks only the first node (Node) of that specific bin using Java's standard synchronized keyword. Other threads can write to other bins concurrently, ensuring high parallel write throughput.


πŸ“Š Architectural Blueprint: Retrieve and Put Command Execution

To trace how threads interact during map updates, the sequence diagram below maps a parallel read and write operation on the table:

sequenceDiagram
    participant ThreadA as Reader Thread (get)
    participant ThreadB as Writer Thread (put)
    participant Table as Node Array (Volatile)

    ThreadA->>Table: Compute hash, read Bin 0 (lock-free)
    Table-->>ThreadA: Return Node A value (Volatile Read)
    ThreadB->>Table: Compute hash, target Bin 0
    Note over ThreadB: Bin 0 occupied. Lock Node A
    ThreadB->>Table: Synchronized append Node B
    ThreadB->>Table: Release Node A lock

This sequence diagram illustrates the parallel execution of reads and writes in the map. The reader thread accesses Bin 0 through a lock-free volatile read, returning data immediately. Concurrently, the writer thread targets the same bin, locks the head node (Node A) using synchronized block execution, appends Node B, and releases the lock, keeping locking highly localized.


🧠 Deep Dive: Treeification, Resizing, and Thread Helping

As the map handles insertions, it dynamically adjusts its layouts to preserve performance.

The Internals of Dynamic Resizing, ForwardingNodes, and CounterCells

When the number of elements in the map exceeds the threshold (determined by the load factor), the map initiates a resize operation (transfer), doubling the table size.

Unlike standard maps that block all operations during resize, ConcurrentHashMap supports parallel resizing:

  • The resizer thread allocates a new table array in memory.
  • It starts copying nodes from the old table to the new table, processing bins in reverse order.
  • When a bin's nodes have been transferred, the thread inserts a ForwardingNode at the head of that bin in the old table. The ForwardingNode contains a reference to the new table.
  • If another thread attempts to read from a bin containing a ForwardingNode, it is redirected to the new table, ensuring reads remain non-blocking.
  • If another thread attempts to write (put) to a bin containing a ForwardingNode, it suspends its write and joins the resize operation (helpTransfer), copying other bins to accelerate the migration.

To track size concurrently without a global lock contention point on a single volatile counter, ConcurrentHashMap uses a CounterCell array (similar to LongAdder).

When a thread successfully adds or removes a mapping, it attempts to atomically increment a base count using CAS. If the CAS fails (due to write contention from other threads), the thread hashes its Thread ID to pick a slot in the CounterCell array and increments that cell instead.

When you call size(), the map invokes sumCount(), which reads the base count and iterates through the CounterCell array, summing their values. This splits write contention across multiple slots, maintaining high-throughput write performance.

Performance Analysis of ConcurrentHashMap vs SynchronizedMap

The time complexity of lookups in both maps is $O(1)$ on average, and $O(\log N)$ in the worst case (due to Red-Black tree conversion). However, the performance under write concurrency is vastly different.

The table below compares the performance profiles of the three key Java maps.

Map TypeRead LocksWrite LocksScaling FactorConcurrency Level
HashtableReentrant Global LockReentrant Global LockPoor (all threads block)1 (single lock)
SynchronizedMapReentrant Global LockReentrant Global LockPoor (all threads block)1 (single lock)
ConcurrentHashMapLock-Free (Volatile)Bucket-level LockExcellent (linear scaling)Dynamic (equal to number of bins)

🌍 Real-World Implementation: Spring Cache Resolvers

In the Spring Boot framework, cache managers (like ConcurrentMapCacheManager) utilize ConcurrentHashMap as their primary storage engine for local in-memory caching.

Since Spring services resolve cache lookups concurrently across multiple HTTP threads, using ConcurrentHashMap ensures that cache hits return in sub-microseconds without thread serialization, while new cache entries are populated safely and concurrently.


βš–οΈ Trade-offs and Failure Modes: Thread Starvation and CPU Cycles

While highly optimized, the map has minor trade-offs:

  • Size Approximation: The size() method does not acquire a global lock. Instead, it aggregates counter cells using a volatile counter. If updates occur during the scan, the returned size is an approximation, making it unsuitable for strict transactional size checks.
  • CPU Utilization in CAS Loops: If many threads continuously attempt to write to the same empty bucket, the CAS loop will spin repeatedly. This consumes high CPU cycles without completing work (busy spinning).

🧭 Decision Guide: HashMap vs Hashtable vs ConcurrentHashMap

Use this decision table to choose the correct map implementation.

SituationRecommendationAlternative
Single-threaded operations or thread-local storageHashMapAvoids all synchronization overhead.
Multi-threaded read/write workloads with high concurrencyConcurrentHashMapLinear scaling under load.
Legacy code integration requiring basic thread safetyCollections.synchronizedMapOnly use if subclass overrides require strict global locks.

πŸ§ͺ Practical Implementation: Tuning Concurrency Levels and Initial Capacity

To initialize a ConcurrentHashMap with optimized capacity to prevent resize overhead in production:

// Configure map with initial capacity of 100,000 and load factor 0.75
// Concurrency level parameter estimates concurrent writer threads (defaults to 16)
int initialCapacity = 100000;
float loadFactor = 0.75f;
int concurrencyLevel = 32;

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel);

This constructor configuration pre-allocates table bins in memory, preventing expensive resize operations and page migrations during initialization.


πŸ“š Lessons Learned: Concurrency Antipatterns in Java

Avoid these common mistakes when working with concurrent collections:

  1. Non-Atomic Compound Actions: Using separate containsKey() and put() calls on a concurrent map is not atomic:

    // VIOLATION: Another thread can insert a key between containsKey and put!
    if (!map.containsKey(key)) {
        map.put(key, value);
    }
    

    Fix: Use atomic methods like putIfAbsent(), computeIfAbsent(), or merge().

  2. Locking on Map Instances: Never use synchronized(map) to protect compound operations. Since ConcurrentHashMap does not use object-level locks, locking the map instance will not prevent other threads from writing to the bins.
  3. Assuming Iterator Consistency: Iterators returned by ConcurrentHashMap are weakly consistent. They do not throw ConcurrentModificationException if the map is updated during iteration, but they may or may not reflect updates made after the iterator is created.

πŸ“Œ Summary: The ConcurrentHashMap Rules

  • No Global Locks: Localizes locking to individual bucket heads to enable parallel writes.
  • Volatile Reads: Declares internal node fields as volatile to guarantee lock-free read visibility.
  • Atomic CAS Loops: Uses CAS instructions to update empty buckets without thread suspension.
  • Parallel Resizing: Inserts ForwardingNode entries to keep reads non-blocking during table transfers.
  • Atomic Counters: Tracks size splits using thread-local CounterCell counters to avoid contention.

AI-generated article quiz

Test your understanding

🧠

Ready to test what you just learned?

Generate four focused questions from this article. Answers include immediate explanations.

Guided series path

How It Works: Internals Explained

View all lessons β†’
Lesson 24 of 34

Reader feedback

Was this article useful?

Rate it if it helped, then continue with the next deep dive when you are ready.

Sign in to save your rating.