Why HashMap Is Not Thread‑Safe: A Deep Dive into the Source Code
The article explains how HashMap can enter an infinite loop, lose data, or corrupt entries when accessed concurrently, illustrates the root causes with JDK 1.7 and 1.8 source code, compares alternative thread‑safe maps, and shows how to answer this classic interview question.
What Triggers the Crash?
During a production incident the CPU spikes to 100% and the system hangs because a HashMap has turned into a circular linked list, causing an endless traversal.
Living Analogy
Imagine a community parcel locker. Normally you place a package in an empty slot, chain conflicting packages together, and expand the locker when it’s full.
Put a parcel: find an empty slot.
Two conflicting parcels: link them with a chain.
Too many parcels: enlarge the locker.
When two people try to place parcels simultaneously, the locker collapses – this is the essence of HashMap’s thread‑unsafe behavior.
Source Code Broken Down
1. Infinite Loop – Head‑Insertion During Resize (JDK 1.7)
JDK 1.7 uses head‑insertion when moving entries to a new bucket during resize:
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; // record next node
int i = indexFor(e.hash, newCapacity); // compute new index
e.next = newTable[i]; // point to new bucket head
newTable[i] = e; // insert at head
e = next; // move to next
}
}
}If two threads resize concurrently, thread A may finish e.next = newTable[i] and be pre‑empted. Thread B then runs, reversing the chain order. When thread A resumes, it continues with the stale reference, creating a circular list. Subsequent get(key) calls loop forever, maxing out the CPU.
2. Data Loss – Overwrite on put (JDK 1.8)
JDK 1.8 switches to tail‑insertion, eliminating the loop, but the put operation remains unsafe:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab = (Node<K,V>[])table;
int n = (tab = resize()).length; // may trigger resize
int i = (n - 1) & hash; // bucket index
if ((p = tab[i]) == null) {
tab[i] = newNode(hash, key, value, null); // directly insert
}
// ... other branches omitted
}When two threads evaluate tab[i] == null simultaneously, both think the bucket is empty and each creates a new node. The later write overwrites the earlier one, so the first entry is lost.
3. Corrupted Data During Resize
If multiple threads invoke resize() at the same time, they each allocate a new array newTable and copy entries. Their operations overwrite each other’s newTable, resulting in missing or misplaced entries in the final map.
Solution Comparison
ConcurrentHashMap : Uses segment locks and red‑black trees; high performance; requires familiarity with its API. ★★★★★
Collections.synchronizedMap : Simple wrapper around HashMap; suffers from a global lock and poor performance. ⭐⭐
Hashtable : Legacy synchronized map; stable but locks the entire table and disallows null keys. ⭐
Interview Answer
The three thread‑safety issues are: Infinite loop – caused by head‑insertion during resize in JDK 1.7 (fixed in JDK 1.8). Data overwrite – two threads simultaneously see an empty bucket and the later write overwrites the earlier one. Resize corruption – concurrent calls to resize() overwrite each other’s new array. In practice, use HashMap only in single‑threaded contexts and switch to ConcurrentHashMap for concurrent access.
One‑Line Summary
HashMap behaves like a shared parcel locker: one user is fine, but multiple users cause lost parcels and a collapsed rack.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
