Fundamentals 10 min read

Why HashMap Is Not Thread‑Safe: Analysis of JDK 1.7 and JDK 1.8 Behaviors

This article explains why Java's HashMap is inherently thread‑unsafe, detailing how JDK 1.7’s resize operation can create circular linked lists and data loss, while JDK 1.8’s insertion logic can cause overwrites, and provides code examples and step‑by‑step analysis.

Java Captain
Java Captain
Java Captain
Why HashMap Is Not Thread‑Safe: Analysis of JDK 1.7 and JDK 1.8 Behaviors

HashMap is known to be thread‑unsafe, but the exact reasons are often unclear; this article examines the root causes in both JDK 1.7 and JDK 1.8.

1. JDK 1.7 HashMap

In a multithreaded environment JDK 1.7 can enter an infinite loop during resize. The following test code demonstrates the problem:

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 repeatedly eventually produces a dead‑loop situation, sometimes accompanied by an array‑index‑out‑of‑bounds error. Stack traces show that the loop occurs inside HashMap’s transfer method during resizing:

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;
        }
    }
}

The method uses head‑insertion (setting e.next = newTable[i] ) which reverses the order of the linked list. In a multithreaded scenario, one thread may be paused after moving an entry, while another thread completes the resize, leading to a circular list and thus an infinite loop.

1.1 Expansion Causing a Dead Loop

Assume a simple hash function (key % bucketSize) and an initial table size of 2 with keys 3, 7, 5 all landing in bucket 1. After a resize to size 4, two threads A and B perform put operations concurrently. If thread A is paused inside transfer after processing entry 3, thread B finishes the resize, updating the shared newTable . When A resumes, it continues moving entries, eventually linking 7 → 3 and 3 → 7, forming a cycle.

1.2 Expansion Causing Data Loss

A similar interleaving can cause an entry to be lost. After thread A moves entry 7 to the new table and then processes entry 5, the original entry 3 is never re‑inserted, resulting in both a missing element and a circular list.

2. JDK 1.8 HashMap

JDK 1.8 replaces head‑insertion with tail‑insertion and introduces tree bins, eliminating the circular‑list issue. However, the putVal method still suffers from race conditions when two threads compute the same bucket index simultaneously:

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 {
        // ... handling of existing nodes, tree bins, etc.
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

If two threads reach the if (p == null) check at the same time, both will think the bucket is empty and each will insert its own node, causing the later thread to overwrite the earlier one. This demonstrates that HashMap remains unsafe for concurrent writes even after JDK 1.8’s optimizations.

Conclusion

HashMap’s thread‑unsafe nature manifests in two ways:

In JDK 1.7, resizing can create circular linked lists or lose entries, leading to infinite loops.

In JDK 1.8, concurrent put operations can overwrite each other, still violating thread safety.

Therefore, HashMap should never be used without external synchronization in multithreaded contexts.

JavaConcurrencyThread SafetyData StructureHashMapJDK1.7JDK1.8
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.