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:
HashMapis a single-person fitting room in a clothing store: if two people enter at once, chaos ensues.ConcurrentHashMapis 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)
- Lock-Free Reads (
get): Operates without any locks. Thevalandnextfields inside eachNodeare declaredvolatile, establishing a strict Happens-Before memory visibility relationship across CPU cores. - Lock-Free Insertion on Empty Buckets: Uses the hardware Compare-And-Swap (CAS) processor instruction (
casTabAtviaVarHandle/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! - 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:
ForwardingNodeMarker: As soon as an individual bucket is migrated tonextTable, the old bucket is replaced with aForwardingNode(special hash-1).- Thread Assistance (
helpTransfer): When another writing thread encounters aForwardingNode, it does not block waiting for the resize to finish; it actively assists in migrating remaining buckets! - Coordinated Strides via
transferIndex: A global atomic counter assigns chunks of buckets (strides of 16–64 buckets) to helper threads. - Lock-Free Read Redirection: Queries invoking
get()on aForwardingNodeare immediately forwarded tonextTablewith 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
baseCountvia CAS. - Under high thread contention, threads hash to distinct slots in
counterCellsusingThreadLocalRandom.getProbe()and increment their dedicated cell. size()andmappingCount()returnbaseCountsummed across allcounterCellswithout acquiring locks.
Weakly Consistent Iterators
Iterators returned by ConcurrentHashMap (iterator(), spliterator()) exhibit Weakly Consistent semantics:
- They never throw
ConcurrentModificationException. - They can traverse the table concurrently with active writes.
- They reflect the state of the map at or after iterator creation and may (or may not) reflect subsequent modifications.
- 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:
- The table array is declared
transient volatile Node<K,V>[] table;and element reads use volatile memory access (Unsafe.getObjectVolatile/VarHandle). - 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.
final int hash;andfinal 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: Passingnullkeys or values immediately throwsNullPointerException. -
❌ “Checking
containsKey()beforeput()is thread-safe on ConcurrentHashMap.”
Correction: Composite operations are non-atomic and requirecomputeIfAbsentorputIfAbsent.
4. Related Topics
- How HashMap Works in a Multi-threaded Environment — Multi-threaded concurrency bugs
- Can HashMap Store a null Key — Null handling rules
- What is the Difference Between HashMap and Hashtable — Hashtable legacy differences
- Which Map Implementation to Choose — Map selection matrix