Q14 of 40 · Core Java

What's the difference between HashMap and ConcurrentHashMap? How does ConcurrentHashMap achieve thread safety?

Core JavaMidhashmapconcurrenthashmapconcurrencycollectionsthread-safety

Short answer

Short answer: HashMap is not thread-safe — concurrent modifications can corrupt its internal structure or cause infinite loops during resize. ConcurrentHashMap allows safe concurrent reads and writes via per-bin synchronisation and CAS operations, without locking the entire map.

Detail

In single-threaded code, HashMap is faster and uses less memory. The moment you share it across threads without external synchronisation, behaviour becomes undefined: two simultaneous puts during a resize can corrupt the internal hash table, iterating while another thread inserts can throw ConcurrentModificationException, and reads can return stale or null values even when an entry was just inserted.

ConcurrentHashMap (Java 8+) uses a table of bins. Empty bins are written with CAS (compare-and-swap — a single atomic CPU instruction) requiring no lock. Non-empty bins are locked at the individual bin level using synchronized on the bin's head node, not on the whole map. This means two threads writing to different bins run truly concurrently. Reads never lock.

Important caveats:

  • size() and isEmpty() are approximate during concurrent modification — they return a best-effort count, not an exact snapshot.
  • Iterators are weakly consistent: they don't throw ConcurrentModificationException but may or may not reflect insertions that happened after the iterator was created.
  • Compound operations like "check-then-act" still need atomic methods: use compute(), merge(), computeIfAbsent() rather than get() followed by put().

Prefer ConcurrentHashMap over Collections.synchronizedMap(new HashMap<>()) — the synchronised wrapper locks the entire map for every operation, defeating the purpose of fine-grained concurrency. Use ConcurrentHashMap when multiple threads write; use a plain read-only Map.copyOf() or Map.of() when the map is built once and then only read.

// EXAMPLE

ConcurrentMap.java

import java.util.concurrent.ConcurrentHashMap;

// ❌ Race condition — HashMap under concurrent writes
Map<String, Integer> counts = new HashMap<>();
// Two threads calling counts.put() simultaneously can corrupt the table

// ✅ Thread-safe with fine-grained locking
Map<String, Integer> counts = new ConcurrentHashMap<>();

// ❌ Still a race: get() then put() is not atomic
Integer current = counts.get("requests");
counts.put("requests", (current == null ? 0 : current) + 1);

// ✅ Atomic compute — read-modify-write in one operation
counts.compute("requests", (key, val) -> val == null ? 1 : val + 1);

// ✅ Even cleaner with merge
counts.merge("requests", 1, Integer::sum);

// computeIfAbsent — initialise a list per key thread-safely
Map<String, List<String>> grouped = new ConcurrentHashMap<>();
grouped.computeIfAbsent("smoke", k -> new java.util.concurrent.CopyOnWriteArrayList<>())
       .add("TC_001");

// WHAT INTERVIEWERS LOOK FOR

Why HashMap is unsafe (not just 'it's not thread-safe' but the actual failure modes), how ConcurrentHashMap achieves safety without full-map locking, the approximate-size caveat, and use of atomic compound operations. Strong answers distinguish from Collections.synchronizedMap.

// COMMON PITFALL

Using Collections.synchronizedMap as the preferred solution. It works but serialises all access through a single lock — worse throughput than ConcurrentHashMap under any contention. Also: forgetting that compute/merge are needed for compound operations even on ConcurrentHashMap.