Back to HashMap Internals
Q1Question 1 of 10

What is the difference between HashMap, Hashtable, and ConcurrentHashMap?

Key Concepts
  • HashMap is not thread-safe, allows one null key and multiple null values, and is the fastest map for single-threaded use
  • Hashtable is legacy (Java 1.0), synchronizes every method on a single lock, does not allow null keys or values, and should never be used in new code
  • ConcurrentHashMap (Java 5+) uses per-bucket locking (Java 8+), provides lock-free reads via volatile, and does not allow null keys or values
  • ConcurrentHashMap provides atomic compound operations (computeIfAbsent, merge, compute) that eliminate race conditions without external synchronization
  • ConcurrentHashMap's iterator is weakly consistent and never throws ConcurrentModificationException; HashMap's iterator is fail-fast
  • Under contention, ConcurrentHashMap is 4-10x faster than Hashtable because it locks only the specific bucket being written, not the entire map
  • Null is disallowed in ConcurrentHashMap because get() returning null would be ambiguous: does the key not exist, or does it map to null?

Definition

HashMap is fast and not thread-safe; Hashtable is legacy with coarse locking; ConcurrentHashMap is the modern thread-safe map with per-bucket locking and lock-free reads.
  • HashMap (Java 1.2): not thread-safe, allows one null key and multiple null values, fail-fast iterator, best performance for single-threaded code
  • Hashtable (Java 1.0): every method synchronized on one lock, does not allow null keys or values, 2-4x slower than ConcurrentHashMap under contention
  • ConcurrentHashMap (Java 5+): per-bucket locking (Java 8: CAS + synchronized on first node), lock-free reads, weakly consistent iterator, atomic compound operations
  • Rule: use HashMap for single-threaded code, ConcurrentHashMap for multi-threaded code, never Hashtable

HashMap is a bank ledger with no lock – the fastest to use, but only one person should use it at a time. Hashtable is a ledger with one giant lock on the whole book – only one person can read or write at any time, even if ten people want to read different pages. ConcurrentHashMap is a ledger with a lock on each individual page – ten people can read and write different pages simultaneously, and reading never requires a lock at all.

Understanding the Concepts

What is the difference between HashMap, Hashtable, and ConcurrentHashMap?

These three classes all implement the Map interface, but they differ in thread safety, null handling, performance, and design era.

| Feature | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| Introduced | Java 1.2 | Java 1.0 | Java 5 |
| Thread-safe | No | Yes (coarse lock) | Yes (fine-grained) |
| Null key | 1 allowed | Not allowed | Not allowed |
| Null value | Multiple allowed | Not allowed | Not allowed |
| Locking | None | Entire map (one lock) | Per-bucket (Java 8+) |
| Read locking | None | Same global lock | None (volatile reads) |
| Iterator | Fail-fast (throws CME) | Fail-fast (Enumeration) | Weakly consistent |
| Atomic ops | No | No | Yes (computeIfAbsent, merge) |
| Performance | Fastest (no sync) | Slowest (global lock) | Fast (concurrent) |
| Recommendation | Single-threaded | Never use | Multi-threaded |

JAVA
package com.bank.demo;

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

public class MapComparisonDemo {
    public static void main(String[] args) {
        // HashMap – fast, not thread-safe
        Map<String, BigDecimal> hashMap = new HashMap<>();
        hashMap.put("SAV2001", new BigDecimal("150000.00"));
        hashMap.put(null, BigDecimal.ZERO);          // null key OK
        hashMap.put("CUR3001", null);                // null value OK
        System.out.println("HashMap: " + hashMap);

        // ConcurrentHashMap – thread-safe, no null
        Map<String, BigDecimal> concurrentMap = new ConcurrentHashMap<>();
        concurrentMap.put("SAV2001", new BigDecimal("150000.00"));
        try {
            concurrentMap.put(null, BigDecimal.ZERO); // NullPointerException!
        } catch (NullPointerException e) {
            System.out.println("CHM rejects null key");
        }
        try {
            concurrentMap.put("CUR3001", null);       // NullPointerException!
        } catch (NullPointerException e) {
            System.out.println("CHM rejects null value");
        }
    }
}
TEXT
HashMap: {null=0, SAV2001=150000.00, CUR3001=null}
CHM rejects null key
CHM rejects null value

Why is Hashtable slow?

Every public method in Hashtable is declared synchronized. This means that before any operation – even size() or get() – the calling thread must acquire the object's intrinsic lock.

In single-threaded code, this lock acquisition and release is pure waste. In multi-threaded code, all threads compete for a single lock on the entire table. While one thread is reading, no other thread can read or write anything.

JAVA
// Hashtable internals (simplified)
public class Hashtable<K,V> {
    public synchronized V get(Object key) { /* ... */ }    // locked
    public synchronized V put(K key, V value) { /* ... */ } // locked
    public synchronized V remove(Object key) { /* ... */ }  // locked
    public synchronized int size() { /* ... */ }            // even size!
    public synchronized boolean contains(Object value) { /* ... */ } // locked
}
// 10 threads reading 10 different keys? Queued behind ONE lock.

Why is ConcurrentHashMap fast?

ConcurrentHashMap (Java 8+) uses a fundamentally different approach:

  • Reads are completely lock-free. The get() method reads the bucket array and node values using volatile semantics. Multiple threads can read simultaneously without any coordination.
  • Writes to empty buckets use CAS. When inserting into an empty bucket, ConcurrentHashMap uses Compare-And-Swap (a hardware-level atomic operation) – no lock at all.
  • Writes to non-empty buckets lock only that bucket. The synchronized block is on the first node of the specific bucket, not on the entire map. Other buckets remain accessible.

This means that with 1000 buckets, 1000 threads can potentially write to 1000 different buckets simultaneously.

JAVA
package com.bank.service;

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

public class ConcurrentMapDemo {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, BigDecimal> balances = new ConcurrentHashMap<>();

        // Lock-free read
        balances.put("SAV2001", new BigDecimal("150000"));
        BigDecimal bal = balances.get("SAV2001"); // no lock, volatile read

        // Atomic compound operations
        balances.computeIfAbsent("CUR3001", k -> BigDecimal.ZERO);
        balances.merge("SAV2001", new BigDecimal("5000"), BigDecimal::add);
        System.out.println("After merge: " + balances.get("SAV2001"));

        // Safe iteration – weakly consistent, never throws CME
        balances.forEach((account, balance) ->
            System.out.println(account + ": " + balance));
    }
}
TEXT
After merge: 155000
CUR3001: 0
SAV2001: 155000

Why does ConcurrentHashMap not allow null?

In a concurrent context, get(key) returning null is ambiguous. Does it mean the key does not exist, or does the key exist but maps to null? In a single-threaded HashMap, you can call containsKey() to resolve this ambiguity. In a concurrent map, the state can change between get() and containsKey(), so the ambiguity cannot be resolved safely.

Doug Lea (the author of ConcurrentHashMap) made the design decision to reject null keys and values to avoid this ambiguity.

JAVA
// The ambiguity problem:
Map<String, String> map = new ConcurrentHashMap<>();
String value = map.get("SAV2001"); // returns null
// Does "SAV2001" not exist? Or does it map to null?
// In a concurrent context, containsKey() might return a stale answer.
// Solution: reject null entirely.

What are the atomic compound operations in ConcurrentHashMap?

ConcurrentHashMap provides several methods that perform read-modify-write as a single atomic operation, eliminating race conditions:

JAVA
package com.bank.service;

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

public class AtomicOpsDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, BigDecimal> balances = new ConcurrentHashMap<>();

        // computeIfAbsent: insert only if key absent (atomic)
        balances.computeIfAbsent("SAV2001", k -> new BigDecimal("50000"));
        balances.computeIfAbsent("SAV2001", k -> new BigDecimal("99999")); // skipped
        System.out.println("After computeIfAbsent: " + balances.get("SAV2001"));

        // merge: combine old and new values (atomic)
        balances.merge("SAV2001", new BigDecimal("10000"), BigDecimal::add);
        System.out.println("After merge: " + balances.get("SAV2001"));

        // compute: update value based on key and current value (atomic)
        balances.compute("SAV2001", (k, v) ->
            v == null ? BigDecimal.ZERO : v.subtract(new BigDecimal("5000")));
        System.out.println("After compute: " + balances.get("SAV2001"));

        // putIfAbsent: insert only if key absent (returns existing or null)
        BigDecimal existing = balances.putIfAbsent("CUR3001", new BigDecimal("75000"));
        System.out.println("putIfAbsent returned: " + existing); // null (was absent)

        // replace: replace only if key exists
        boolean replaced = balances.replace("CUR3001",
            new BigDecimal("75000"), new BigDecimal("80000")); // CAS-style
        System.out.println("Replaced: " + replaced);
    }
}
TEXT
After computeIfAbsent: 50000
After merge: 60000
After compute: 55000
putIfAbsent returned: null
Replaced: true

What happens if you use HashMap in multi-threaded code?

Using a plain HashMap across threads without synchronization causes silent data corruption. There are no exceptions – the data is just wrong.

  • Lost updates: two threads put different values for the same key simultaneously; one update is silently lost.
  • Infinite loop (Java 7): concurrent resize could create a cycle in the linked list, causing get() to loop forever, consuming 100% CPU.
  • Corrupted structure: entries end up in wrong buckets, size counter becomes incorrect, or entries become unreachable.

Java 8 HashMap uses a different resize algorithm that avoids the infinite loop, but lost updates and corrupted state are still possible.

JAVA
// WRONG – HashMap across threads without synchronization
Map<String, Integer> counts = new HashMap<>();
// Thread 1: counts.put("SAV2001", counts.getOrDefault("SAV2001", 0) + 1);
// Thread 2: counts.put("SAV2001", counts.getOrDefault("SAV2001", 0) + 1);
// Both read 0, both write 1 → count is 1, should be 2. LOST UPDATE.

// CORRECT – ConcurrentHashMap with atomic merge
ConcurrentHashMap<String, Integer> safeCounts = new ConcurrentHashMap<>();
// Thread 1: safeCounts.merge("SAV2001", 1, Integer::sum);
// Thread 2: safeCounts.merge("SAV2001", 1, Integer::sum);
// Both execute atomically → count is 2. CORRECT.

What are common mistakes?

Mistake 1: Using Hashtable because it is "thread-safe"

JAVA
// WRONG – Hashtable is legacy, slow, and compound operations are still racy
Map<String, String> map = new Hashtable<>();
if (!map.containsKey("SAV2001")) {
    map.put("SAV2001", "Rajesh"); // RACE CONDITION between check and put!
}

// CORRECT – ConcurrentHashMap with atomic operation
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.putIfAbsent("SAV2001", "Rajesh"); // atomic check-and-put

Mistake 2: Wrapping ConcurrentHashMap with Collections.synchronizedMap

JAVA
// WRONG – defeats the purpose of ConcurrentHashMap
Map<String, String> map = Collections.synchronizedMap(new ConcurrentHashMap<>());
// Now EVERY operation goes through a single synchronized block – slow!

// CORRECT – use ConcurrentHashMap directly
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

Mistake 3: Storing null in ConcurrentHashMap

JAVA
// WRONG – throws NullPointerException
concurrentMap.put("SAV2001", null);

// CORRECT – use a sentinel value or Optional
concurrentMap.put("SAV2001", BigDecimal.ZERO); // sentinel for "no balance"

What about iteration behavior?

  • HashMap: fail-fast iterator. If the map is structurally modified during iteration (except through the iterator's own remove), it throws ConcurrentModificationException.
  • ConcurrentHashMap: weakly consistent iterator. It never throws CME. It reflects the state of the map at some point during iteration and may or may not reflect modifications made after the iterator was created.
JAVA
package com.bank.demo;

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

public class IteratorBehaviorDemo {
    public static void main(String[] args) {
        // HashMap: fail-fast
        Map<String, String> hashMap = new HashMap<>(Map.of("A", "1", "B", "2"));
        try {
            for (String key : hashMap.keySet()) {
                hashMap.put("C", "3"); // ConcurrentModificationException!
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("HashMap: CME during iteration");
        }

        // ConcurrentHashMap: weakly consistent
        ConcurrentHashMap<String, String> chm = new ConcurrentHashMap<>(Map.of("A", "1", "B", "2"));
        for (String key : chm.keySet()) {
            chm.put("C", "3"); // no exception! Weakly consistent.
        }
        System.out.println("CHM after iteration: " + chm);
    }
}
TEXT
HashMap: CME during iteration
CHM after iteration: {A=1, B=2, C=3}

What should a senior engineer know?

- ConcurrentHashMap.size() vs mappingCount(). size() returns an int, which overflows for maps with more than Integer.MAX_VALUE entries. mappingCount() returns a long. Under concurrency, both are approximate – they reflect the count at some point during execution.

- ConcurrentHashMap and streams. CHM provides reduceValues(), searchKeys(), and forEachEntry() that run in parallel using the ForkJoinPool. These are more efficient than creating a stream from entrySet().

- Hashtable was retrofitted. Hashtable was made to implement Map when the Collections Framework was introduced in Java 1.2. Its original API used Enumeration instead of Iterator.

- IdentityHashMap. Uses reference equality (==) instead of equals() for key comparison. Useful for serialization frameworks and class-loading scenarios.

- WeakHashMap. Keys are held via WeakReference. When a key is no longer strongly referenced elsewhere, the entry is automatically removed by GC. Useful for caches where you want entries to disappear when the key objects are garbage collected.

What are common interview follow-up questions?

  • "How does ConcurrentHashMap achieve thread safety without locking reads?" – Volatile reads of node values.
  • "What happens if you use HashMap concurrently without synchronization?" – Silent data corruption, lost updates, potentially infinite loop (Java 7).
  • "Why doesn't ConcurrentHashMap allow null?" – Ambiguity: get() returning null could mean absent or mapped-to-null.
  • "What is the difference between computeIfAbsent and putIfAbsent?" – computeIfAbsent takes a function and only calls it if key is absent; putIfAbsent takes the value directly.

Visual Overview

DIAGRAM
  Locking comparison

  Hashtable (single lock):
  ┌──────────────────────────────────┐
  │ Thread-1: get("A") → LOCK whole  │
  │ Thread-2: put("B") → WAIT        │
  │ Thread-3: size()   → WAIT        │
  │ All operations serialized!       │
  └──────────────────────────────────┘

  ConcurrentHashMap (per-bucket):
  ┌──────────────────────────────────┐
  │ Bucket 0: Thread-1 writes → LOCK │
  │ Bucket 1: Thread-2 writes → LOCK │
  │ Bucket 2: Thread-3 reads  → FREE │ ← no lock for reads!
  │ Bucket 3: Thread-4 reads  → FREE │
  │ All proceed in parallel!          │
  └──────────────────────────────────┘

  Null handling:
  HashMap:           null key OK, null value OK
  Hashtable:         null key NO, null value NO
  ConcurrentHashMap: null key NO, null value NO

Key Takeaway

HashMap is the fastest map for single-threaded code. It has no synchronization overhead, allows one null key and multiple null values, and uses a fail-fast iterator. But use it across threads without synchronization and you get silent data corruption – not exceptions, just wrong data.

Hashtable was Java's first thread-safe map, but its design is fundamentally flawed. It synchronizes every method on a single lock, so even 10 threads reading 10 different entries are serialized. It is 2-4x slower than ConcurrentHashMap under contention. Never use it in new code.

ConcurrentHashMap is the modern solution. It reads without any locking (volatile reads), writes to empty buckets without locking (CAS), and writes to non-empty buckets by locking only the specific bucket. It provides atomic compound operations like computeIfAbsent and merge that eliminate race conditions. Its weakly consistent iterator never throws ConcurrentModificationException.

In the National Bank of India's real-time balance cache, Rajesh switched from Hashtable to ConcurrentHashMap and saw throughput jump from 500K to 2M lookups per second. The key was eliminating the single-lock bottleneck.

Remember one sentence: HashMap for single-threaded, ConcurrentHashMap for multi-threaded, never Hashtable.

---

Share
Use ← → arrow keys to navigate