🔑 Section 10 · Question #23

What is ConcurrentHashMap and How Does It Differ from HashMap

ConcurrentHashMap is a high-throughput, thread-safe implementation of the ConcurrentMap (and Map) interface, designed for concurrent access across hundreds of threads without lo...


🟢 Junior Level

Short Interview Answer (30 seconds)

ConcurrentHashMap is a high-throughput, thread-safe implementation of the ConcurrentMap (and Map) interface, designed for concurrent access across hundreds of threads without locking the entire table.

Key Differences from HashMap

Characteristic HashMap ConcurrentHashMap
Thread Safety ❌ Not thread-safe (data races, data loss) ✅ Thread-safe (highly concurrent)
null Support ✅ Allows 1 null key and multiple null values ❌ Throws NullPointerException
Iterator Semantics Fail-fast (throws ConcurrentModificationException) Weakly Consistent (never throws CME)
Single-Threaded Speed Maximum (zero synchronization overhead) Negligibly lower (nanosecond differences)
Concurrent Writes Severe corruption and lost updates Safe, high-throughput parallel writes

Code Demonstration in Java 21

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class ConcurrentMapDemo {
    public static void main(String[] args) {
        ConcurrentMap<String, Integer> stock = new ConcurrentHashMap<>();

        stock.put("MacBook", 10);

        // Atomic compound update without external locks:
        stock.merge("MacBook", 1, Integer::sum); // Value is now 11
        System.out.println("Updated stock: " + stock.get("MacBook")); // 11

        // Attempting to insert null immediately throws NullPointerException:
        // stock.put(null, 5); // Runtime exception!
    }
}

Real-World Analogy: HashMap is a single-person fitting room in a clothing store: if two people enter at once, chaos ensues. ConcurrentHashMap is a grand coatroom with dozens of independent attendant windows: each visitor is served at their own counter without queuing up behind others.


🟡 Middle Level

Multi-Threaded Architecture in Java 8+

In Java 7, ConcurrentHashMap was divided into 16 fixed segments (Segment[] extending ReentrantLock), restricting write concurrency to 16 threads.

In Java 8+, segments were replaced with Bucket-Level Locking:

[Bucket 0] ──► Node (Lock-free insertion via CAS)
[Bucket 1] ──► synchronized (firstNode) { [Node] -> [Node] -> [Node] }
[Bucket 2] ──► ForwardingNode (Routes concurrent queries to nextTable during resize)
[Bucket 3] ──► TreeBin (Read/write lock wrapper for Red-Black tree)
  1. Lock-Free Reads (get): Operates without any locks. The val and next fields inside each Node are declared volatile, establishing a strict Happens-Before memory visibility relationship across CPU cores.
  2. Lock-Free Insertion on Empty Buckets: Uses the hardware Compare-And-Swap (CAS) processor instruction (casTabAt via VarHandle/Unsafe) to insert the head node without acquiring a monitor:
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
        break; // Successfully inserted lock-free!
    
  3. Collision Locking: If a bucket is already occupied, the thread locks only the head node using synchronized (firstNode). Other threads can concurrently write to any other bucket in the table.

Why Are null Keys and Values Forbidden?

Architect Doug Lea intentionally excluded null to eliminate ambiguity in multi-threaded programs.

In a single-threaded HashMap, map.get("key") == null can be disambiguated:

if (map.get(key) == null) {
    if (map.containsKey(key)) {
        // Key exists, mapped to null
    } else {
        // Key is missing
    }
}

In a concurrent environment, another thread could insert or remove the key between get() and containsKey(). This causes a Time-Of-Check to Time-Of-Use (TOCTOU) race condition. Prohibiting null guarantees that if get(key) == null, the key is unequivocally absent.


Atomic Compound Operations

Standard composite blocks like if (!map.containsKey(k)) map.put(k, v); suffer from TOCTOU race conditions. ConcurrentHashMap provides atomic alternatives:

  • putIfAbsent(key, value): Atomically inserts only if key is not present.
  • computeIfAbsent(key, mappingFunction): Atomically computes and inserts the value if key is absent (ideal for concurrent caches).
  • merge(key, value, remappingFunction): Atomically combines existing and new values (ideal for counters and metrics).

🔴 Senior Level

Cooperative Parallel Resizing (helpTransfer)

Unlike HashMap (where a single thread performs the entire $O(N)$ resize), ConcurrentHashMap implements distributed cooperative resizing:

  1. ForwardingNode Marker: As soon as an individual bucket is migrated to nextTable, the old bucket is replaced with a ForwardingNode (special hash -1).
  2. Thread Assistance (helpTransfer): When another writing thread encounters a ForwardingNode, it does not block waiting for the resize to finish; it actively assists in migrating remaining buckets!
  3. Coordinated Strides via transferIndex: A global atomic counter assigns chunks of buckets (strides of 16–64 buckets) to helper threads.
  4. Lock-Free Read Redirection: Queries invoking get() on a ForwardingNode are immediately forwarded to nextTable with zero blocking latency.

High-Throughput Sizing: CounterCell (LongAdder Pattern)

Updating a single AtomicLong counter across hundreds of threads causes severe CPU Cache Line Bouncing as CPU cores repeatedly invalidate L1/L2 caches.

ConcurrentHashMap implements the LongAdder sharded cell pattern:

private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
  • Threads first attempt to update baseCount via CAS.
  • Under high thread contention, threads hash to distinct slots in counterCells using ThreadLocalRandom.getProbe() and increment their dedicated cell.
  • size() and mappingCount() return baseCount summed across all counterCells without acquiring locks.

Weakly Consistent Iterators

Iterators returned by ConcurrentHashMap (iterator(), spliterator()) exhibit Weakly Consistent semantics:

  1. They never throw ConcurrentModificationException.
  2. They can traverse the table concurrently with active writes.
  3. They reflect the state of the map at or after iterator creation and may (or may not) reflect subsequent modifications.
  4. They do not allocate full snapshot copies, operating with $O(1)$ memory overhead.

🎯 Interview Cheat Sheet

1. 30-Second Elevator Pitch

ConcurrentHashMap is a high-throughput, thread-safe hash table utilizing lock-free reads (volatile fields), CAS for empty buckets, and fine-grained synchronized locks on individual bucket heads. It forbids null keys and values to prevent concurrent ambiguity. It features cooperative multi-threaded resizing via ForwardingNodes, scalable sizing via CounterCells (LongAdder), and Weakly Consistent iterators that never throw ConcurrentModificationException.


2. 4 Tricky Interview Questions with Deep Answers

Question 1: Why is ConcurrentHashMap.get() completely lock-free while guaranteeing visibility of fresh writes?

Answer: get() requires no synchronized or explicit locks because:

  1. The table array is declared transient volatile Node<K,V>[] table; and element reads use volatile memory access (Unsafe.getObjectVolatile / VarHandle).
  2. Inside Node<K,V>, fields are declared volatile:
    volatile V val;
    volatile Node<K,V> next;
    

    Under the Java Memory Model (JMM), writes to volatile fields establish a Happens-Before relationship with subsequent reads, ensuring CPU cores do not read stale cached values from L1/L2 caches.

  3. final int hash; and final K key; fields are immutable once initialized.

Question 2: Why is if (!map.containsKey(k)) map.put(k, v); unsafe even when using ConcurrentHashMap?

Answer: While containsKey() and put() are individually thread-safe and atomic, their composition is not atomic. Between containsKey() returning false and put() executing, the calling thread can be suspended by the OS. Another thread can insert a value for key k. When the first thread resumes, it overwrites the newly inserted value. This is a classic Check-Then-Act (TOCTOU) race condition. Use atomic compound operations like map.putIfAbsent(k, v) or map.computeIfAbsent(k, ...).

Question 3: What does size() return in ConcurrentHashMap under thousands of concurrent writes? Is it guaranteed exact?

Answer: size() (and mappingCount()) returns an estimate, not a point-in-time snapshot. It sums baseCount and all CounterCell[] values without locking the map. By the time the sum finishes, concurrent worker threads have already added or removed elements. Consequently, application logic should never rely on map.size() == 0 for thread coordination; use map.isEmpty() instead.

Question 4: Can a Deadlock occur when using computeIfAbsent() in ConcurrentHashMap?

Answer: Yes, it can. During computeIfAbsent(), the thread locks the target bucket monitor (synchronized (node)). If the provided mapping lambda attempts to mutate the same ConcurrentHashMap (targeting the same or a dependent bucket), a self-deadlock or IllegalStateException occurs:

map.computeIfAbsent("key1", k -> {
    // 💥 MUTATING THE MAP INSIDE THE LAMBDA:
    map.put("key2", 42); 
    return 100;
});

The JDK javadoc strictly prohibits mutating map operations within callback functions.


3. Common Pitfalls & Red Flags

  • ❌ “ConcurrentHashMap uses 16 Segments in Java 21.”
    Correction: Segments were removed in Java 8 and replaced with fine-grained bucket-level locks and CAS operations.

  • ❌ “ConcurrentHashMap iterators create a complete cloned snapshot of the table.”
    Correction: They are Weakly Consistent iterators that traverse live bucket arrays without cloning.

  • ❌ “Null keys land in bucket 0 in ConcurrentHashMap.”
    Correction: Passing null keys or values immediately throws NullPointerException.

  • ❌ “Checking containsKey() before put() is thread-safe on ConcurrentHashMap.”
    Correction: Composite operations are non-atomic and require computeIfAbsent or putIfAbsent.