Why HashMap Is Not Thread‑Safe in JDK 1.7 and JDK 1.8: Analysis of Resizing and Concurrency Issues
This article explains why Java's HashMap is unsafe in multithreaded environments, detailing how JDK 1.7's resize operation can create circular linked lists and data loss, and how JDK 1.8's tail‑insertion still suffers from element overwriting under concurrent puts.
HashMap is known to be non‑thread‑safe, but the exact reasons are often unclear; this article dissects the problem for both JDK 1.7 and JDK 1.8.
1. JDK 1.7 HashMap
In JDK 1.7, concurrent put operations can trigger a dead loop during table resizing. The following test program creates five threads that continuously invoke put on a shared HashMap :
public class HashMapTest {
public static void main(String[] args) {
HashMapThread thread0 = new HashMapThread();
HashMapThread thread1 = new HashMapThread();
HashMapThread thread2 = new HashMapThread();
HashMapThread thread3 = new HashMapThread();
HashMapThread thread4 = new HashMapThread();
thread0.start();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
class HashMapThread extends Thread {
private static AtomicInteger ai = new AtomicInteger();
private static Map
map = new HashMap<>();
@Override
public void run() {
while (ai.get() < 1000000) {
map.put(ai.get(), ai.get());
ai.incrementAndGet();
}
}
}Running this code several times on a multi‑core machine produces dead‑loop symptoms and occasional array‑index errors, as shown by the captured stack traces.
The root cause lies in the transfer method used during resizing. In JDK 1.7 the method moves entries with a head‑insertion strategy, which reverses the linked‑list order:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry
e : table) {
while (null != e) {
Entry
next = e.next;
if (rehash) {
e.hash = (null == e.key) ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}When two threads (A and B) concurrently resize, thread A may be paused after reading an entry but before updating the new table, while thread B completes the resize. Because the head‑insertion reverses the order, the partially transferred list can form a cycle, leading to an infinite loop during subsequent traversals.
A similar interleaving can cause data loss: after thread B finishes resizing, thread A resumes and inserts entries based on stale pointers, overwriting or discarding existing nodes and leaving the map in an inconsistent state.
2. JDK 1.8 HashMap
JDK 1.8 replaces head‑insertion with tail‑insertion, eliminating the circular‑list problem. The core putVal method is shown below:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node
[] tab; Node
p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node
e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode
)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}Although the linked list is no longer reversed, the method still performs a simple put without any synchronization. If two threads compute the same hash and both find the bucket empty, they will both insert their node, and the later thread will overwrite the earlier one, causing lost updates.
Summary
In JDK 1.7, concurrent resizing can create circular linked lists or lose entries because the transfer method uses head‑insertion.
In JDK 1.8, the head‑insertion bug is fixed, but the map remains non‑thread‑safe; concurrent put operations may overwrite each other.
Therefore, HashMap should never be used without external synchronization in multithreaded scenarios.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.