What is ConcurrentHashMap
Prior to ConcurrentHashMap, thread safety for maps was achieved in two legacy ways:
🟢 Junior Level
1. What is ConcurrentHashMap
ConcurrentHashMap (CHM) is a high-throughput, thread-safe implementation of the ConcurrentMap interface (which extends Map) in the Java Collections Framework (java.util.concurrent), specifically engineered for highly concurrent multi-threaded environments.
Prior to ConcurrentHashMap, thread safety for maps was achieved in two legacy ways:
Hashtable(Java 1.0).Collections.synchronizedMap(new HashMap<>())(Java 1.2).
Both legacy approaches rely on Coarse-Grained Locking across the entire map instance. Every method (get, put, remove, size) acquires a single monitor lock on the whole object. As a result, reading threads block writing threads and vice versa, creating severe contention bottlenecks that prevent applications from scaling on multi-core processors.
ConcurrentHashMap eliminates this bottleneck:
- Read operations (
get) are completely Lock-Free, proceeding concurrently without blocking readers or writers. - Write operations (
put,remove) lock only the specific bucket being modified, allowing concurrent threads to write to different buckets simultaneously.
// Thread-safe concurrent map instantiation:
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Multiple threads can write and read simultaneously without external synchronized blocks:
concurrentMap.put("alpha", 1);
concurrentMap.computeIfAbsent("beta", k -> 2);
Integer value = concurrentMap.get("alpha"); // Lock-free read!
2. Implementation Comparison Matrix
| Criterion | HashMap |
Collections.synchronizedMap |
ConcurrentHashMap |
|---|---|---|---|
| Thread Safety | ❌ None | ✅ Yes (Single global lock) | ✅ Yes (Fine-grained per-bucket) |
| Read Synchronization | None | Locks entire map | Lock-Free (Zero locks) |
| Write Synchronization | None | Locks entire map | Locks only the target bucket head |
null Key / Value |
Key: ✅, Val: ✅ | Key: ✅, Val: ✅ | Key: ❌, Val: ❌ (NullPointerException) |
| Iterator Behavior | Fail-Fast (CME) |
Fail-Fast (requires external lock) | Weakly Consistent (Never throws CME) |
🟡 Middle Level
1. Why ConcurrentHashMap Strictly Prohibits null
Attempting to insert null as either a key or a value into a ConcurrentHashMap throws an immediate NullPointerException:
map.put(null, 100); // ❌ Throws NullPointerException!
map.put("key", null); // ❌ Throws NullPointerException!
Architectural Rationale (Doug Lea):
In single-threaded collections like HashMap, calling map.get(key) == null can be disambiguated by checking map.containsKey(key): if containsKey returns true, the value is explicitly null; otherwise, the key is absent.
In concurrent programming, this pattern creates a fatal Race Condition:
// In a concurrent environment, this logic is fundamentally flawed:
if (map.get(key) == null) {
// Another thread can insert or delete the key RIGHT HERE!
if (map.containsKey(key)) {
// This assertion is no longer trustworthy!
}
}
To eliminate this ambiguity (“is the key missing, or is the value null?”), Doug Lea enforced a strict invariant: null keys and values are strictly prohibited across all modern concurrent collections.
2. Atomic Compound Actions
A widespread pitfall in concurrent programming is the illusion of safety when executing multiple independent map calls (Check-Then-Act sequences):
// ❌ ANTI-PATTERN: Race condition between two non-atomic calls!
if (!map.containsKey("counter")) {
map.put("counter", 1); // A concurrent thread can intervene between containsKey and put!
}
// ✅ ATOMIC: ConcurrentMap atomic methods
map.putIfAbsent("counter", 1);
// ✅ ATOMIC MUTATION: In-place lambda calculation (Java 8+)
map.compute("counter", (k, v) -> (v == null) ? 1 : v + 1);
// ✅ ATOMIC ACCUMULATION: Atomic merge counter
map.merge("counter", 1, Integer::sum);
3. Weakly Consistent Iterators
Iterators returned by ConcurrentHashMap.keySet(), values(), and entrySet() are Weakly Consistent:
- They never throw
ConcurrentModificationException. - They reflect the state of the map at the moment the iterator was constructed and are guaranteed to traverse all entries present at creation time.
- They may (but are not guaranteed to) reflect insertions, updates, or removals performed by concurrent threads after iterator creation.
- They guarantee each entry is delivered at most once.
🔴 Senior Level
1. Architectural Evolution: Java 7 vs. Java 8+
Java 7: Segment-Level Locking (16 ReentrantLocks)
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ Segment 0 │ │ Segment 1 │ │ Segment 2 │ │ Segment 3 │ ... (Concurrency capped at 16)
└───────────┘ └───────────┘ └───────────┘ └───────────┘
Java 8+: Node-Level Locking + CAS
[Bucket 0] -> Empty? Atomic CAS insertion (Lock-free!)
[Bucket 1] -> Populated? Synchronize strictly on the single bucket HEAD node!
[Bucket 2] -> Concurrent read? Lock-free volatile read!
- Java 7: Sliced the table into 16 discrete
Segmentclasses (each subclassingReentrantLock), providing a fixed concurrency level of 16. - Java 8+: Completely eliminated
Segments. Empty buckets are initialized using lock-free CAS (Compare-And-Swap) instructions. Collisions synchronize solely on the head node of that specific bucket (synchronized(node)), scaling concurrent write capacity directly with table capacity.
2. Distributed Size Tracking: CounterCell Array & mappingCount()
In high-throughput systems, maintaining a single shared volatile int size would create an extreme cache-coherence bottleneck (continuous CPU cache line invalidation across cores).
ConcurrentHashMap implements an internal stripped counter mechanism identical to LongAdder:
- A base counter field
volatile long baseCount. - A striped array of counter cells
volatile CounterCell[] counterCells. - When threads update entries without contention, they increment
baseCountvia CAS. - Under high contention, threads hash their thread IDs to increment an individual cell in
counterCells. mappingCount()sumsbaseCountand active cells, returning alongvalue suitable for collections containing $> 2^{31}$ entries:
// Preferred over size() for massive distributed maps:
long totalEntries = map.mappingCount();
3. Cooperative Multi-Threaded Resizing (ForwardingNode)
When resizing from $N$ to $2N$ buckets:
ConcurrentHashMapinserts a special marker node calledForwardingNodewith hash-1(MOVED) into completed buckets.- If another thread attempts to write to a bucket currently undergoing migration, it detects the
ForwardingNodeand immediately callshelpTransfer(). - Multiple writing threads actively cooperate to migrate chunks of buckets in parallel, eliminating prolonged GC or resizing pauses.
4. The computeIfAbsent() Deadlock Hazard
While computeIfAbsent() provides atomic convenience, executing blocking logic or nested map mutations inside the lambda can cause severe production outages:
// ❌ HAZARD 1: Thread-Pool Starvation via Blocking I/O
map.computeIfAbsent("token", k -> {
return httpClient.fetchRemoteToken(); // ⚠️ DANGEROUS! Holds bucket monitor for seconds!
});
// ❌ HAZARD 2: Guaranteed Deadlock
Map<Integer, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent(1, k1 -> {
return map.computeIfAbsent(2, k2 -> 42); // ⚠️ DEADLOCK if keys map to the same bucket chain!
});
4 Tricky Questions
1. Why does ConcurrentHashMap.size() return an approximate count rather than an exact atomic snapshot?
Answer:
In a highly concurrent system, obtaining an exact instantaneous count requires acquiring a global lock across all buckets, halting every writing thread and destroying throughput.
ConcurrentHashMap sums baseCount and the striped CounterCell[] array on-the-fly without locking. Because concurrent threads continue inserting and deleting elements while the sum loop executes, the returned size represents a weakly consistent estimate accurate to the nanosecond of execution, which is sufficient for monitoring and capacity planning.
2. Why did Doug Lea strictly prohibit null keys and values in ConcurrentHashMap, while standard HashMap allows them?
Answer:
In single-threaded code, map.get(k) == null can be disambiguated with map.containsKey(k) to check if the value itself was null.
In a multi-threaded application, get() followed by containsKey() creates a non-atomic race condition: between the two calls, a peer thread could insert or delete the key, making the check unreliable. By forbidding null values entirely, map.get(k) == null unambiguously means one thing: the key is absent.
3. What architectural differences exist between ConcurrentHashMap in Java 7 and Java 8+?
Answer:
- Java 7: Used Segment-Level Locking. The map was divided into a fixed array of 16
Segmentobjects, each extendingReentrantLock. Concurrency was bounded by the segment count (16 by default). Memory overhead was high due to nested segment objects. - Java 8+: Adopted Node-Level Locking + CAS. Segments were eliminated. Unoccupied buckets are populated via lock-free CPU CAS instructions. Populated buckets are locked using the Java built-in monitor of the bucket’s head node (
synchronized(firstNode)). Bucket lists convert to Red-Black Trees (TreeBin) upon reaching 8 collisions, and resizing is performed cooperatively viaForwardingNode.
4. How can computeIfAbsent() inadvertently cause an application freeze or deadlock in a high-concurrency production service?
Answer:
The mapping function passed to computeIfAbsent() executes inside the synchronized block of that specific bucket’s head node.
If the mapping function:
- Performs slow I/O, database calls, or remote HTTP requests, the entire bucket remains locked, stalling all other threads trying to access keys in that bucket.
- Mutates or queries the same
ConcurrentHashMaprecursively, and the nested key maps to the same bucket, the thread attempts to acquire a monitor it is waiting on in a non-reentrant state, causing a permanent Deadlock.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“
ConcurrentHashMapis a high-performance thread-safe map implementingConcurrentMap. UnlikeHashtableorCollections.synchronizedMapwhich lock the entire object,ConcurrentHashMapprovides completely lock-free reads usingvolatilereferences and fine-grained per-bucket writes using CAS for empty slots andsynchronized(headNode)for collisions. It strictly prohibitsnullkeys and values to prevent multi-threaded check-then-act race conditions. Iterators are weakly consistent and never throwConcurrentModificationException. For scalable sizing, it uses a stripedCounterCellarray (similar toLongAdder), accessible viamappingCount().”
High-Impact Architectural Facts
- Lock-Free Reads:
get()requires zero locks;table,val, andnextarevolatile, satisfying the JMM Happens-Before relationship. - Node-Level Locking (Java 8+): No Segments; writes synchronize strictly on the collision head node, with empty buckets populated via atomic CAS.
- Null Safety:
nullkeys and values throwNullPointerExceptionby design. - Parallel Resizing: Writers encountering a
ForwardingNode(hash = -1) help transfer entries in parallel viahelpTransfer(). - Large Capacity:
mappingCount()returnslongfor maps exceeding $2\text{ billion}$ entries.
Common Pitfalls & Red Flags
- ❌ Assuming
ConcurrentHashMaplocks segments in modern Java: Segments were removed in Java 8 (2014); modern CHM uses node-level locking. - ❌ Performing blocking I/O inside
computeIfAbsent: Holds the bucket monitor lock, exhausting thread pools. - ❌ Using compound check-then-act logic:
if (!map.containsKey(k)) map.put(k, v)is not atomic; always useputIfAbsent(),compute(), ormerge(). - ❌ Inserting
null: Causes immediateNullPointerException.
Related Topics
- How does ConcurrentHashMap ensure thread-safety — Low-level CAS & locking details
- What is Map and what implementations exist — Comprehensive map taxonomy
- When to use synchronized collections — Synchronized wrapper comparison
- What is CopyOnWriteArrayList — Concurrent List mechanics