Back to HashMap Internals
Q2Question 2 of 10

How does HashMap differ from ConcurrentHashMap?

Key Concepts
  • HashMap is not thread-safe and allows null keys/values; ConcurrentHashMap is thread-safe with per-bucket locking and rejects null
  • HashMap's fail-fast iterator throws ConcurrentModificationException on structural change; CHM's weakly consistent iterator never throws CME
  • ConcurrentHashMap provides atomic compound operations (computeIfAbsent, merge, compute) that are impossible to achieve safely with HashMap without external synchronization
  • HashMap under concurrent modification causes silent data corruption: lost updates, wrong counts, and potentially infinite loops in Java 7
  • ConcurrentHashMap reads are completely lock-free using volatile semantics; HashMap reads have no overhead but are unsafe without synchronization
  • For known single-threaded use, HashMap is slightly faster because it has zero synchronization overhead

Definition

HashMap is faster but not thread-safe and allows null; ConcurrentHashMap is thread-safe with lock-free reads, per-bucket write locking, atomic compound operations, and no null.
  • HashMap: not thread-safe, allows one null key and multiple null values, fail-fast iterator, no atomic compound operations
  • ConcurrentHashMap: thread-safe (per-bucket locking + CAS), no null keys/values, weakly consistent iterator, atomic operations (computeIfAbsent, merge, compute)
  • HashMap under concurrent access causes data corruption silently; ConcurrentHashMap handles concurrent access correctly
  • HashMap is the choice for single-threaded code; ConcurrentHashMap is the choice for multi-threaded code

HashMap is like a private office desk – you work fast because nobody else is there, but if two people use it at the same time, papers get mixed up and lost. ConcurrentHashMap is like a shared filing system with individual drawer locks – multiple people use it simultaneously without interfering, and reading a drawer never requires a key.

Understanding the Concepts

How does HashMap differ from ConcurrentHashMap?

The core difference is thread safety, but the implications go much deeper than just adding locks.

| Feature | HashMap | ConcurrentHashMap |
|---|---|---|
| Thread-safe | No | Yes (per-bucket) |
| Null key | 1 allowed | Not allowed (NPE) |
| Null value | Multiple allowed | Not allowed (NPE) |
| Read locking | None | None (volatile) |
| Write locking | None | CAS or sync(bucket) |
| Iterator | Fail-fast (CME) | Weakly consistent |
| computeIfAbsent | Not atomic | Atomic |
| merge | Not atomic | Atomic |
| Concurrent resize | No | Yes (multi-thread) |
| Performance (single) | Fastest | Slightly slower |
| Performance (multi) | Unsafe | Excellent |

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.*;

public class HashMapVsCHMDemo {
    public static void main(String[] args) throws InterruptedException {
        // Demonstrate the lost-update problem with HashMap
        Map<String, Integer> unsafeMap = new HashMap<>();
        unsafeMap.put("SAV2001", 0);

        // 10 threads each incrementing 1000 times = expected 10000
        int threads = 10;
        int increments = 1000;
        CountDownLatch latch = new CountDownLatch(threads);

        for (int t = 0; t < threads; t++) {
            new Thread(() -> {
                for (int i = 0; i < increments; i++) {
                    // RACE CONDITION: read-modify-write is not atomic
                    unsafeMap.put("SAV2001", unsafeMap.get("SAV2001") + 1);
                }
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("HashMap (expected 10000): " + unsafeMap.get("SAV2001"));

        // Same test with ConcurrentHashMap + atomic merge
        ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();
        safeMap.put("SAV2001", 0);
        CountDownLatch latch2 = new CountDownLatch(threads);

        for (int t = 0; t < threads; t++) {
            new Thread(() -> {
                for (int i = 0; i < increments; i++) {
                    safeMap.merge("SAV2001", 1, Integer::sum); // atomic!
                }
                latch2.countDown();
            }).start();
        }
        latch2.await();
        System.out.println("CHM (expected 10000): " + safeMap.get("SAV2001"));
    }
}
TEXT
HashMap (expected 10000): 6847
CHM (expected 10000): 10000

Why are HashMap's compound operations unsafe?

The classic "check-then-act" pattern is not atomic in HashMap. Between the check and the action, another thread can modify the map.

JAVA
// WRONG – race condition with HashMap
Map<String, List<String>> txnsByAccount = new HashMap<>();
// Thread 1 and Thread 2 both add a transaction for SAV2001:
if (!txnsByAccount.containsKey("SAV2001")) {       // both threads see: absent
    txnsByAccount.put("SAV2001", new ArrayList<>()); // both create a new list
}                                                    // Thread 2's list overwrites Thread 1's
txnsByAccount.get("SAV2001").add("TXN-001");         // Thread 1's transaction is LOST

// CORRECT – atomic with ConcurrentHashMap
ConcurrentHashMap<String, List<String>> safeTxns = new ConcurrentHashMap<>();
safeTxns.computeIfAbsent("SAV2001", k -> new CopyOnWriteArrayList<>())
        .add("TXN-001"); // atomic: list created only once

What is the iterator behavior difference?

HashMap's iterator is fail-fast: if the map is structurally modified during iteration (except through the iterator's own remove), it throws ConcurrentModificationException. This is a safety mechanism, not a guarantee – it detects corruption on a best-effort basis.

ConcurrentHashMap's iterator is weakly consistent: it never throws CME, it may or may not reflect modifications made after the iterator was created, and it traverses elements as they existed at some point during iteration.

JAVA
package com.bank.demo;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class IteratorDifferenceDemo {
    public static void main(String[] args) {
        // HashMap: modifying during iteration throws CME
        Map<String, String> hm = new HashMap<>(Map.of("A", "1", "B", "2", "C", "3"));
        try {
            for (var entry : hm.entrySet()) {
                if (entry.getKey().equals("B")) {
                    hm.remove("A"); // structural modification
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("HashMap: " + e.getClass().getSimpleName());
        }

        // ConcurrentHashMap: safe to modify during iteration
        ConcurrentHashMap<String, String> chm =
            new ConcurrentHashMap<>(Map.of("A", "1", "B", "2", "C", "3"));
        for (var entry : chm.entrySet()) {
            if (entry.getKey().equals("B")) {
                chm.remove("A"); // no exception
            }
        }
        System.out.println("CHM after: " + chm);
    }
}
TEXT
HashMap: ConcurrentModificationException
CHM after: {B=2, C=3}

What are common mistakes?

Mistake: Using HashMap with multiple threads and assuming it works

JAVA
// WRONG – no exceptions thrown, but data is silently corrupted
Map<String, BigDecimal> balances = new HashMap<>();
// Thread 1: balances.put("SAV2001", new BigDecimal("50000"));
// Thread 2: balances.put("CUR3001", new BigDecimal("75000"));
// Possible outcomes: one entry lost, wrong bucket, size=1 instead of 2

// CORRECT
ConcurrentHashMap<String, BigDecimal> balances = new ConcurrentHashMap<>();

What should a senior engineer know?

- HashMap can be faster for read-heavy single-threaded code. ConcurrentHashMap has a small overhead from volatile reads and CAS operations. For pure single-threaded benchmarks, HashMap is 5-15% faster.

- ConcurrentHashMap.computeIfAbsent is blocking. When one thread is inside the compute function for a key, another thread calling computeIfAbsent for the SAME key will block until the first finishes. This prevents the compute function from being called twice, but long computations can create bottlenecks.

- HashMap size is exact; CHM size is approximate. HashMap.size() returns the exact count. ConcurrentHashMap.size() may be stale under concurrent modification because it reads a counter that is updated asynchronously.

- Serialization. Both are serializable. ConcurrentHashMap serializes using a custom format that reconstructs the internal structure during deserialization.

What are common interview follow-up questions?

  • "Can you make HashMap thread-safe?" – Yes, with Collections.synchronizedMap or external synchronization, but ConcurrentHashMap is better.
  • "What happens if two threads put to HashMap at the same time?" – Lost update, corrupted structure, or infinite loop (Java 7).
  • "What is the difference between computeIfAbsent and putIfAbsent?" – computeIfAbsent takes a function (lazy), putIfAbsent takes the value (eager).
  • "Is ConcurrentHashMap size() accurate?" – Approximate under concurrency; use mappingCount() for long values.

Visual Overview

DIAGRAM
  HashMap vs ConcurrentHashMap under concurrent writes

  HashMap (unsafe):
  Thread-1: read count=0 ─┐
  Thread-2: read count=0 ─┤  ← both read 0
  Thread-1: write count=1 │
  Thread-2: write count=1 ┘  ← Thread-1's update LOST
  Result: count=1 (should be 2)

  ConcurrentHashMap (safe):
  Thread-1: merge(key, 1, Integer::sum) → atomic read+modify+write
  Thread-2: merge(key, 1, Integer::sum) → waits if same bucket, then atomic
  Result: count=2 (correct)

Key Takeaway

HashMap and ConcurrentHashMap serve different worlds. HashMap is the fastest map when only one thread accesses it. ConcurrentHashMap handles concurrent access correctly with per-bucket locking and lock-free reads.

The most dangerous misconception is that HashMap "works" in multi-threaded code because it does not throw exceptions. It silently corrupts data: lost updates, wrong counts, entries in wrong buckets. In Java 7, concurrent resize could even cause infinite loops that freeze the JVM.

ConcurrentHashMap solves this with atomic compound operations. The merge method atomically reads the current value, applies a function, and stores the result. The computeIfAbsent method atomically checks for absence and inserts. These operations eliminate the race conditions that plague check-then-act patterns with HashMap.

In the National Bank of India's transaction counter, Rajesh used HashMap and found that 10 threads each incrementing 1000 times produced only 6847 instead of 10000. Switching to ConcurrentHashMap with merge gave exactly 10000 every time.

Remember one sentence: HashMap is fast but breaks silently under concurrent access; ConcurrentHashMap is the only correct choice for multi-threaded maps.

---

Share
Use ← → arrow keys to navigate