What is ConcurrentHashMap and How Does It Differ from HashMap?
In a multi-threaded environment, get(key) = null is ambiguous:
🟢 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
- CAS (Compare-And-Swap) — no lock needed for empty bucket
- synchronized on bucket head — only on collisions
- 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()afterget()— 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:
volatilearray — visibility guaranteesvolatileNode 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
valis 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
- Default choice for concurrency — ConcurrentHashMap
- Atomic operations —
compute,mergeinstead of check-then-act - 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.
- Bulk operations —
forEach,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]]