How HashMap Works in a Multi-threaded Environment
HashMap is not thread-safe. It was engineered for maximum performance in single-threaded contexts and contains zero synchronization (synchronized), atomic operations (CAS), or m...
🟢 Junior Level
Short Interview Answer (30 seconds)
HashMap is not thread-safe. It was engineered for maximum performance in single-threaded contexts and contains zero synchronization (synchronized), atomic operations (CAS), or memory visibility barriers (volatile).
If multiple threads concurrently read and mutate a HashMap, the application enters an Undefined Behavior state:
- Silent Lost Updates: Two threads writing to the same bucket can overwrite each other’s nodes without warning.
- Corrupted Size Counter: The
sizecounter is incremented non-atomically (++size), causingmap.size()to fall out of sync with actual bucket contents. - Phantom
nullReads:map.get(key)can returnnullfor a key that definitely exists due to CPU cache lag or concurrent table resizing. ConcurrentModificationException: Fail-fast iterators detect structural changes and crash immediately.
Concurrency Bug Demonstration in Java 21
import java.util.HashMap;
import java.util.Map;
public class ConcurrencyBugDemo {
public static void main(String[] args) throws InterruptedException {
Map<Integer, Integer> map = new HashMap<>();
// Launch two concurrent worker threads inserting data:
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10_000; i++) map.put(i, i);
});
Thread t2 = new Thread(() -> {
for (int i = 10_000; i < 20_000; i++) map.put(i, i);
});
t1.start();
t2.start();
t1.join();
t2.join();
// Expected 20,000 entries, but actual size is unpredictable (e.g. 18,432)!
System.out.println("Actual map size: " + map.size());
}
}
Real-World Analogy: A
HashMapis like a shared blackboard in a classroom without rules. If two people try to write chalk notes in the exact same spot at the same time, they write over each other’s letters, leaving behind an illegible mess.
🟡 Middle Level
Anatomy of Multi-Threaded Race Conditions
1. Race Condition During Empty Bucket Insertion
Inside putVal(), the bucket null-check and node assignment are non-atomic:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 💥 DATA RACE!
If Thread 1 and Thread 2 access empty bucket tab[i] simultaneously:
- Both threads observe
tab[i] == null. - Thread 1 instantiates its node and assigns it to
tab[i]. - Thread 2 immediately overwrites
tab[i]with its own node. - Outcome: Thread 1’s node is wiped out without error and collected as garbage.
2. Table Structural Corruption During Concurrent resize()
When concurrent threads exceed threshold simultaneously:
- Both threads trigger
resize(), independently allocating two separatenewTabarrays. - Nodes are concurrently re-linked across the two arrays.
- The
tablereference is updated to one array, while the other is abandoned, losing thousands of records.
Thread-Safe Alternatives Compared
| Collection | Synchronization Mechanism | Scalability | Recommended Use Case |
|---|---|---|---|
ConcurrentHashMap |
Lock-free reads, CAS for empty bins, synchronized head locks | Extremely High | Industry standard for multi-threaded applications |
Collections.synchronizedMap(map) |
Coarse-grained wrapper mutex on the whole map | Low (threads queue up) | Simple legacy code with low contention |
Hashtable |
synchronized keyword on every method |
Low (deprecated) | Do not use in modern codebases |
🔴 Senior Level
The Java 7 Catastrophe: Circular Links and 100% CPU
In Java 7, HashMap resized buckets using Head Insertion in the transfer() method, reversing element order:
// Java 7 transfer() method:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next; // Thread 1 paused here!
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
Hang Scenario:
- A bucket contains
[A] -> [B] -> null. - Thread 1 captures
e = A,next = B, then the OS pauses Thread 1. - Thread 2 completes resizing, reversing the list:
[B] -> [A] -> null. - Thread 1 wakes up with stale references and links
A.next = newTable[i], and in the next step linksB.next = A. - Outcome: A circular link forms:
[A] <-> [B]. - Subsequent
get()calls on that bucket enter an infinite loop (while (e != null) e = e.next), pinning CPU cores at 100% utilization.
The Java 8+ Myth: Is 100% CPU Truly Solved?
In Java 8+, linked list resizing switched to Tail Insertion (preserving node sequence), eliminating circular loops in standard linked list chains.
However, the claim that HashMap can never hang in Java 8+ is false:
- When a bucket exceeds 8 elements and converts to a Red-Black tree (
TreeNode). - Concurrent unsynchronized writes modify tree pointers (
parent,left,right) during rebalancing (balanceInsertion). - Race conditions corrupt pointer relationships, creating cyclical tree structures (
node.parent.left == node). - Subsequent
root(),find(), orget()calls get trapped traversing the corrupt tree, still pinning CPU cores at 100%!
Java Memory Model (JMM) and Safe Publication
In HashMap, critical fields are explicitly non-volatile:
transient Node<K,V>[] table;
transient int size;
transient int modCount;
Under the Java Memory Model:
- Writes performed by one thread establish no Happens-Before relationship with reads in other threads.
- Processors and JIT compilers are permitted to reorder instructions.
- Threads can read stale data indefinitely from L1/L2 core caches or observe partially constructed objects.
Safe Publication Pattern for Read-Only Maps:
A standard HashMap can be safely read concurrently without locks if populated beforehand and published safely:
public class ImmutableConfigRegistry {
// 1. Immutable final field guarantees JMM freeze semantics:
private final Map<String, String> config;
public ImmutableConfigRegistry(Map<String, String> rawData) {
Map<String, String> temp = new HashMap<>(rawData);
// Wrap with unmodifiable view to prevent accidental writes:
this.config = Collections.unmodifiableMap(temp);
}
public String get(String key) {
return config.get(key); // 100% thread-safe without locks
}
}
🎯 Interview Cheat Sheet
1. 30-Second Elevator Pitch
HashMap is not thread-safe: it lacks synchronization, volatile fields, and CAS operations. Concurrent modifications cause lost updates, corrupted size, and phantom null reads. While Java 8+ tail-insertion eliminated circular loops in linked lists, concurrent writes to treeified buckets (TreeNode) can still corrupt tree pointers and trigger 100% CPU infinite loops. Always use ConcurrentHashMap for concurrent write workloads.
2. 4 Tricky Interview Questions with Deep Answers
Question 1: Is the 100% CPU infinite loop bug completely gone in Java 8+?
Answer:
No, it is not.
While Java 8+ eliminated circular linked list loops via tail-insertion, it introduced Red-Black trees (TreeNode). Under concurrent unsynchronized writes, tree balancing algorithms concurrently mutate parent, left, and right references. This causes cyclical tree structures where a node becomes its own ancestor. Subsequent calls to get(), find(), or iterators enter an infinite loop traversing the tree, still pegging CPU cores at 100%.
Question 2: Does Collections.synchronizedMap(map) prevent ConcurrentModificationException during a for-each loop?
Answer:
No, it does not.
Collections.synchronizedMap synchronizes individual method calls (get, put, remove). However, a for-each loop compiles into multiple sequential calls to iterator.hasNext() and iterator.next(). The map lock is released between iterations. If another thread mutates the map between those calls, modCount increments and the iterator throws ConcurrentModificationException. To iterate safely, developers must manually synchronize on the wrapper map:
synchronized (syncMap) {
for (Map.Entry<K, V> e : syncMap.entrySet()) { /* Safe */ }
}
Question 3: Does the fail-fast mechanism guarantee detecting all concurrent modifications in HashMap?
Answer:
No, it does not.
The JDK specification explicitly states that fail-fast behavior is provided on a best-effort basis. The modCount field is not declared volatile. An increment to modCount by one thread can remain buffered in that CPU core’s L1 cache and not immediately propagate to the thread running the iterator. The iterator may read corrupted data instead of throwing the exception.
Question 4: Why is private Map<String, Object> cache = new HashMap<>() inside a Spring @Service a critical production bug?
Answer:
Spring beans default to Singleton scope (one instance shared across the entire application). Web servers (Tomcat, Jetty) process incoming HTTP requests across a thread pool. Consequently, hundreds of concurrent request threads execute the service methods simultaneously, mutating the shared HashMap. This leads to lost updates, broken bucket chains, memory leaks, and production CPU freezes. A ConcurrentHashMap or a dedicated cache (Caffeine) must be used.
3. Common Pitfalls & Red Flags
-
❌ “HashMap is thread-safe as long as threads write to different keys.”
Correction: Concurrent writes triggeringresize()corrupt the entire table structure regardless of the keys being written. -
❌ “Fail-fast iterators make HashMap safe for multi-threaded reads.”
Correction: Fail-fast is a debugging aid, not a thread-safety mechanism.modCountis not volatile. -
❌ “Java 8 completely fixed the 100% CPU bug in HashMap.”
Correction: Tail-insertion fixed linked lists, but concurrent modifications inTreeNodetrees still produce cyclical loops and 100% CPU lockups. -
❌ “Collections.synchronizedMap provides the same performance as ConcurrentHashMap.”
Correction:synchronizedMaplocks the entire collection on every operation, creating a global contention bottleneck.
4. Related Topics
- What is ConcurrentHashMap and How Does It Differ from HashMap — Concurrent collection architecture
- What Happens During Rehashing — Resize mechanics
- What is the Difference Between HashMap and Hashtable — Legacy synchronized tables
- What is race condition — Race conditions and concurrency basics