JavaCollectionsadvanced
Updated:

HashMap vs ConcurrentHashMap: Thread Safety Explained

7 min read

What actually goes wrong when threads share a HashMap, how ConcurrentHashMap's fine-grained locking fixes it, and how to choose between them.

TL;DR – Quick Answer

HashMap is not thread-safe: concurrent writes can lose updates and corrupt internal state. ConcurrentHashMap is built for concurrency — reads are lock-free and writes lock only the affected bucket, so threads rarely block each other. Use HashMap for single-threaded code and ConcurrentHashMap whenever multiple threads read and write the same map.

On This Page

Every Java backend eventually shares a map across threads — a cache, a session store, a metrics counter. Pick HashMap for that job and you get bugs that pass every test on your laptop and surface only under production load. Pick ConcurrentHashMap and the same code just works.

This comparison shows exactly what breaks, why, and how ConcurrentHashMap fixes it without the performance penalty of the old synchronized approach. It assumes you know how HashMap works internally — buckets, chains, resizing — because the thread-safety story happens inside those structures.

Verdict at a glance

Dimension HashMap ConcurrentHashMap
Thread-safe no yes
Read performance fastest near-identical, lock-free
Write locking none (unsafe) per-bucket (CAS + synchronized on bucket head)
Null key / values one null key, null values OK both forbidden
Iterator fail-fast (throws CME) weakly consistent (never throws CME)
Atomic compound ops no (has the methods, not atomic) yes — putIfAbsent, compute, merge are atomic
size() exact estimate under concurrent updates
Since Java 1.2 Java 5 (rewritten in Java 8)
Use when single thread / thread-confined shared mutable state

Verdict: thread-confined map → HashMap. Map shared by concurrently writing threads → ConcurrentHashMap. There is no third case that survives code review.

What actually breaks with a shared HashMap

"HashMap is not thread-safe" stays abstract until you watch it lose data. This program runs two threads, each putting 50,000 distinct keys:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class LostUpdates {
    public static void main(String[] args) throws InterruptedException {
        Map<Integer, Integer> unsafe = new HashMap<>();
        Map<Integer, Integer> safe = new ConcurrentHashMap<>();

        System.out.println("HashMap size:          " + fill(unsafe));
        System.out.println("ConcurrentHashMap size: " + fill(safe));
    }

    static int fill(Map<Integer, Integer> map) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50_000; i++) map.put(i, i);
        });
        Thread t2 = new Thread(() -> {
            for (int i = 50_000; i < 100_000; i++) map.put(i, i);
        });
        t1.start(); t2.start();
        t1.join(); t2.join();
        return map.size();
    }
}

Expected size: 100,000, since every key is unique. The ConcurrentHashMap reports exactly that. The HashMap typically reports less — entries vanish. Why: two threads append to the same bucket simultaneously, each reads the same chain head, and the second write overwrites the first thread's link. During a resize the window gets worse, because entries are being redistributed while another thread inserts.

Java 7's resize could even produce a circular linked list, hanging threads in an infinite get() loop at 100% CPU. Java 8's order-preserving resize removed that specific failure, but lost updates, wrong sizes and broken invariants all remain. The absence of a crash is not the presence of safety.

Common mistake: A common mistake beginners make is assuming a map that is only read by many threads but written during startup is fine as a HashMap. It is fine — but only if the handover between the writing phase and the reading phase has proper synchronization (a final field, a volatile publish, or framework-managed initialization). Without a safe publication point, readers may see a partially built map. The rules come from the Java memory model, covered in synchronization in Java.

How ConcurrentHashMap locks: the Java 8 design

The naive fix — one big lock, as in Hashtable or Collections.synchronizedMap() — makes every thread queue for every operation. ConcurrentHashMap is smarter, and the design changed significantly over time:

Java 5–7: segment locking. The map was split into 16 segments by default, each a small hashtable with its own lock. Sixteen writers could proceed in parallel if they hit different segments.

Java 8+: per-bucket CAS + synchronized. Segments were retired. The map is now a single table, like HashMap's, and locking happens at the finest possible grain:

  1. Insert into an empty bucket → a lock-free compare-and-swap (CAS) places the node. No lock at all.
  2. Write to an occupied bucketsynchronized on that bucket's head node only. Writers to other buckets proceed untouched.
  3. Reads → no locks, ever. Node.val and Node.next are volatile, so readers always see completed writes.
  4. Resize → cooperative: writer threads that notice a resize in progress help transfer buckets instead of blocking behind one resizer.

With the default table of thousands of buckets under real load, two writers colliding on the same bucket at the same instant is rare — so contention stays near zero while correctness stays absolute.

Interview note: "How does ConcurrentHashMap work internally?" is usually a follow-up to the HashMap internals question. The high-value keywords are: CAS for empty buckets, synchronized on the bucket head (not the whole map), volatile reads with no read locks, segment locking as the pre-Java-8 historical answer, and treeification carried over from HashMap. Candidates who still describe 16 segments as the current design date their knowledge to Java 7.

Atomic compound operations: the real superpower

Thread safety per method call is not enough. The classic race is check-then-act:

if (!map.containsKey(key)) {   // thread B can slip in here
    map.put(key, expensiveCreate(key));
}

Even on a fully synchronized map, two threads can both pass the check and both create the value. ConcurrentHashMap's compound methods run the check and the write atomically under the bucket lock:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

public class AtomicOps {
    public static void main(String[] args) throws InterruptedException {
        Map<String, Integer> hits = new ConcurrentHashMap<>();
        CountDownLatch done = new CountDownLatch(4);

        for (int t = 0; t < 4; t++) {
            new Thread(() -> {
                for (int i = 0; i < 25_000; i++) {
                    hits.merge("/home", 1, Integer::sum);  // atomic count
                }
                done.countDown();
            }).start();
        }
        done.await();
        System.out.println(hits.get("/home"));  // always exactly 100000
    }
}

Four threads increment the same counter 100,000 times in total, and the result is exact every run — no synchronized block in sight. The same guarantee backs putIfAbsent() (insert exactly once), computeIfAbsent() (build-once caches) and compute() (read-modify-write). On a plain HashMap, these default methods exist but are not atomic.

One discipline to keep: mapping functions passed to compute*() run while the bucket is locked, so keep them short and never touch the same map inside them.

The null rule and weakly consistent iteration

Two behavioral differences trip up people migrating from HashMap:

No nulls, by design. map.get(k) == null must mean exactly one thing in a concurrent world: the key is absent. Allowing null values would make that ambiguous, and resolving the ambiguity with containsKey() is itself a race. If you stored null to mean "known to be missing", store a sentinel object or use Optional as the value instead.

Weakly consistent iterators. HashMap iterators throw ConcurrentModificationException if the map changes mid-iteration. ConcurrentHashMap iterators never throw — they reflect the map at some point since creation and may or may not show concurrent updates. Related: size() is an estimate while writers are active, so never build logic on an exact live count.

Performance: measure the right thing

Single-threaded, HashMap wins by a small margin — ConcurrentHashMap pays for volatile semantics and CAS bookkeeping even with no contention. That is why the answer is not "use ConcurrentHashMap everywhere".

Multi-threaded, the ranking inverts hard. Under mixed read/write load from several threads, ConcurrentHashMap scales close to linearly with cores because reads never block and writes rarely collide, while Hashtable and synchronizedMap flatline — every operation queues on one lock. (And HashMap is disqualified: fast wrong answers are still wrong.) The HashMap vs Hashtable comparison covers that older single-lock story in depth.

Both classes share the same asymptotics — average O(1) get/put, treeified buckets capping the worst case at O(log n) — so the choice is purely about the threading model, not about big-O.

Choose HashMap if / choose ConcurrentHashMap if

Choose HashMap if:

  • The map lives and dies inside one method, one request, or one thread.
  • A framework guarantees single-threaded access (e.g., a per-request context).
  • You need null values, or the last few percent of single-thread speed.
  • You want the simplest mental model while learning collections.

Choose ConcurrentHashMap if:

  • Multiple threads write the map at any point in its life — caches, registries, counters, session stores.
  • You need atomic check-then-act semantics (putIfAbsent, computeIfAbsent, merge).
  • You iterate while other threads update, and want no ConcurrentModificationException.
  • You are building on thread pools or executors — anything touched by an ExecutorService callback counts as shared state.

For freshers: the 30-second interview answer

"HashMap is not thread-safe — concurrent writes can lose updates and corrupt the bucket structure, and in Java 7 a concurrent resize could even loop forever. ConcurrentHashMap is thread-safe with fine-grained locking: since Java 8 it CASes into empty buckets, synchronizes only on a bucket's head node for writes, and reads are lock-free via volatile fields. It bans nulls to keep get() unambiguous, its iterators are weakly consistent instead of fail-fast, and it adds atomic operations like putIfAbsent and merge. So: HashMap for thread-confined maps, ConcurrentHashMap for shared ones."

If you can then field the follow-ups — why nulls are banned, segments vs CAS, why size() is approximate — you are ahead of most candidates at the 2–4 year level. Drill the whole cluster with the Java collections interview questions, and pressure-test your delivery in a mock interview before the real one.

Frequently Asked Questions

What happens if multiple threads use a plain HashMap?
Concurrent writes can interleave so that one thread's entry silently overwrites another's, sizes become inconsistent, and during a resize the internal table can be observed in a broken state. In Java 7 this could even create an infinite loop; Java 8 removed that failure mode but the map is still unsafe and updates are still lost.
How does ConcurrentHashMap achieve thread safety without locking the whole map?
Since Java 8 it uses per-bucket locking with CAS. An insert into an empty bucket is a lock-free compare-and-swap; updates to an occupied bucket synchronize only on that bucket's first node. Reads take no locks at all because node fields are volatile. Threads block only when they write to the same bucket at the same moment.
Why does ConcurrentHashMap not allow null keys or values?
Because null would be ambiguous under concurrency. If get(key) returned null, you could not tell whether the key was absent or mapped to null, and checking containsKey() separately is a race — another thread can change the map between the calls. Banning nulls makes get()'s answer unambiguous.
Is ConcurrentHashMap better than Collections.synchronizedMap()?
For concurrent workloads, yes. synchronizedMap wraps every method in a single mutex, so all threads serialize and iteration needs manual synchronization. ConcurrentHashMap allows parallel reads and writes, its iterators do not throw ConcurrentModificationException, and it offers atomic operations like putIfAbsent, compute and merge.
Does ConcurrentHashMap make check-then-put code safe?
Not automatically. A containsKey() followed by put() is still two separate operations that another thread can interleave. Safety comes from using the atomic compound methods — putIfAbsent(), computeIfAbsent(), compute() and merge() — which perform the check and the write under the bucket lock as one action.
Should I just use ConcurrentHashMap everywhere to be safe?
No. In single-threaded code its volatile reads, CAS operations and concurrency bookkeeping are pure overhead, and its no-null rule may force awkward workarounds. Use HashMap for thread-confined maps and reach for ConcurrentHashMap only when the map is genuinely shared across threads.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 14 July 2026 LinkedIn
Chat with us