Question 23 · Section 10

What is ConcurrentHashMap and How Does It Differ from HashMap?

In a multi-threaded environment, get(key) = null is ambiguous:

Language versions: English Russian Ukrainian

🟢 Junior Level

ConcurrentHashMap is a thread-safe version of HashMap. Multiple threads can simultaneously read and write data without problems.

Key differences:

HashMap ConcurrentHashMap
Not thread-safe Thread-safe
Faster in single thread Slightly slower, but safe
Allows null keys/values Forbids null
Fail-fast iterator Weakly Consistent iterator (no CME)

Example:

// HashMap — crashes in multi-threaded environment
Map<String, Integer> unsafe = new HashMap<>();

// ConcurrentHashMap — safe
ConcurrentMap<String, Integer> safe = new ConcurrentHashMap<>();
safe.put("key1", 100);
safe.putIfAbsent("key2", 200); // Atomic operation

When to use: When multiple threads access the map simultaneously.

🟡 Middle Level

ConcurrentHashMap Architecture (Java 8+)

Characteristic HashMap ConcurrentHashMap
Thread safety No Yes
Locking No Per-bucket (synchronized on bucket head)
Null keys/values Allowed Forbidden (NPE)
Iterator Fail-fast Weakly Consistent (snapshot at creation time)
Atomic operations No putIfAbsent, computeIfAbsent, merge

How Thread Safety Works

  1. CAS (Compare-And-Swap) — no lock needed for empty bucket
  2. synchronized on bucket head — only on collisions
  3. volatile fields — visibility guarantees between threads
// CAS for empty bucket:
if (casTabAt(tab, i, null, newNode))
    break; // Success, without locking!

// synchronized on collision:
synchronized (f) {
    // Adding to list/tree
}

Why is Null Forbidden?

In a multi-threaded environment, get(key) = null is ambiguous:

  • Key absent? Or value = null?
  • Can’t atomically check containsKey() after get() — state may change

Atomic Operations

map.putIfAbsent("key", 100);     // Check + insert = atomic
map.computeIfAbsent("key", k -> computeExpensive(k)); // Lazy computation
map.merge("key", 1, Integer::sum); // Atomic update

Iteration: Weakly Consistent

for (Map.Entry<String, Integer> e : map.entrySet()) {
    // Won't throw ConcurrentModificationException
    // May not see changes made after iterator creation
}

🔴 Senior Level

Internal Implementation (Java 8+)

transient volatile Node<K,V>[] table;

Key differences from HashMap:

  • volatile array — visibility guarantees
  • volatile Node fields (val, next) — happens-before
  • ForwardingNode during resize — signals progress

Multi-threaded Resize

// On resize, multiple threads can help:
private transient volatile int transferIndex;

Threads coordinate via transferIndex, each processing its own bucket range. This speeds up resize in high-load systems.

Lock Striping vs Node-level Locking

Approach Java 7 (Segment) Java 8+ (Node)
Granularity 16 segments Each bucket
Concurrency Up to 16 threads Up to capacity threads
Overhead Lower Minimal

Memory Ordering

ConcurrentHashMap uses full memory barrier via volatile:

  • Write to val is visible to all threads
  • Happens-before between put and get from different threads
  • No stale reads

Why No Null?

Liveness Problem in multi-threading:

// Potential scenario without null ban:
if (map.get(key) == null) {      // null or no key?
    if (!map.containsKey(key)) { // No key... or was it removed between calls?
        map.put(key, value);     // TOCTOU race!
    }
}

Benchmark

Metric HashMap ConcurrentHashMap
get (1 thread) 5 ns 8 ns
get (8 threads) N/A (race) 60 ns
put (1 thread) 8 ns 15 ns
put (8 threads) N/A (race) 120 ns

Overhead ~60% on single-thread, but on 8 threads — the only correct option.

Production Best Practices

  1. Default choice for concurrency — ConcurrentHashMap
  2. Atomic operationscompute, merge instead of check-then-act
  3. size() in Java 8+ uses a CounterCell system and works in O(1) in the common case. The result is exact, but may be stale by the time it’s used — don’t use in hot path.
  4. Bulk operationsforEach, search, reduce (Java 8+) with parallelism threshold

🎯 Interview Cheat Sheet

Must know:

  • ConcurrentHashMap — thread-safe via CAS + synchronized on bucket head
  • Forbids null keys/values — eliminates ambiguity in check-then-act
  • Weakly Consistent iterator — snapshot at creation time, doesn’t throw CME
  • Atomic operations: putIfAbsent, computeIfAbsent, merge — replace check-then-act
  • Java 8+: node-level locking (earlier Java 7: segment-level, 16 segments)
  • size() via CounterCell: O(1), exact but may be stale
  • Multi-threaded resize: threads help via transferIndex

Common follow-up questions:

  • Why is null forbidden? — get=null is ambiguous (no key or value=null?), breaks putIfAbsent
  • How does Weakly Consistent differ from Fail-safe? — Weakly Consistent: sees changes after creation, but doesn’t guarantee completeness
  • Why not synchronizedMap? — single mutex for everything; CHM = concurrent access to different buckets
  • Overhead vs HashMap? — ~60% on single-thread, but the only correct option for multi-thread

Red flags (DO NOT say):

  • “ConcurrentHashMap uses volatile Node fields” — no, CAS + memory barriers
  • “Fail-safe iterator” — correct term: Weakly Consistent
  • “size() = O(n)” — no, in Java 8+ via CounterCell = O(1)

Related topics:

  • [[22. How HashMap Works in a Multi-threaded Environment]]
  • [[25. Can HashMap Store a null Value]]
  • [[26. What is the Difference Between HashMap and Hashtable]]