HashMap vs ConcurrentHashMap in Java - Complete Guide with Examples

๐Ÿ‘๏ธ 229 Views
|
๐Ÿ“… Aug 28, 2026
|
โฑ๏ธ 14 min read
HashMap vs ConcurrentHashMap in Java - Complete Guide with Examples
Head First Java, Third Edition
โœฆ Developer Pick

Head First Java, Third Edition

Ready to learn Java? This book combines puzzles, strong visuals, mysteries, and soul-searching interviews with famous Java objects to engage you in many different ways.

If you have ever worked on a multi-threaded Java application and seen a ConcurrentModificationException or inconsistent data showing up in your map โ€” there is a good chance the root cause was using a HashMap in a concurrent context where it does not belong. Understanding the difference between HashMap and ConcurrentHashMap is not just a theory question that appears in interviews โ€” it is a practical knowledge gap that causes real production bugs.

In this guide we will cover how both work internally, exactly where HashMap breaks under concurrency, how ConcurrentHashMap solves those problems, and when to use which.

Quick Comparison

HashMap ConcurrentHashMap
Thread Safe โŒ No โœ… Yes
Null Keys โœ… 1 null key allowed โŒ Not allowed
Null Values โœ… Allowed โŒ Not allowed
Performance (single thread) Faster Slightly slower
Performance (multi thread) Unsafe โ€” data corruption Excellent
Locking Mechanism No locking Bucket-level locking
Iterator Fail-fast Fail-safe (weakly consistent)
Introduced in Java 1.2 Java 1.5
Use When Single-threaded Multi-threaded

How HashMap Works Internally

Before understanding the concurrency issues, it helps to understand what HashMap is doing under the hood.

// HashMap internal structure โ€” array of Node buckets
// Node<K, V>[] table โ€” the backing array
// Default initial capacity: 16 buckets
// Default load factor: 0.75
// Resizes (doubles) when 75% full

// When you call map.put("key", "value"):
// 1. Compute hashCode of "key"
// 2. Map hashCode to a bucket index: index = hash & (capacity - 1)
// 3. If bucket is empty โ€” insert directly
// 4. If bucket has entries โ€” check equals() for existing key
//    - Key exists โ†’ update value
//    - Key does not exist โ†’ add to linked list (or tree in Java 8+)

// Java 8 optimization:
// When a single bucket has > 8 entries โ†’ linked list converts to Red-Black Tree
// This improves worst-case from O(n) to O(log n) for hash collisions

HashMap<String, Integer> map = new HashMap<>();
map.put("Randhir", 1); // hash("Randhir") โ†’ bucket 5 (example)
map.put("Priya", 2);   // hash("Priya")   โ†’ bucket 11 (example)
map.put("Amit", 3);    // hash("Amit")    โ†’ bucket 5  (collision โ€” same bucket)

// Internally bucket 5 now has a linked list: Randhir โ†’ Amit

Why HashMap Is NOT Thread-Safe

HashMap performs zero synchronization. When two threads access and modify a HashMap simultaneously, the following problems can occur:

Problem 1 โ€” Data Loss on Concurrent Put

// Thread A and Thread B both try to put a new key at the same time
// Both compute the same bucket index
// Both find the bucket empty at the same moment
// Both write their entry to the bucket
// The second write overwrites the first โ€” one entry is silently lost

// This is a classic race condition โ€” no exception thrown, data just disappears
HashMap<String, Integer> map = new HashMap<>();

Thread t1 = new Thread(() -> map.put("key1", 1));
Thread t2 = new Thread(() -> map.put("key2", 2));

t1.start();
t2.start();
t1.join();
t2.join();

// Expected: map has both key1 and key2
// Possible: one of them is missing โ€” silently

Problem 2 โ€” Infinite Loop During Resize (Java 7 and Earlier)

// When HashMap resizes, it rehashes and moves all entries to a new array
// In Java 7, the rehashing reversed the linked list order in each bucket
// If two threads triggered resize simultaneously:
// Thread A builds: A โ†’ B โ†’ C
// Thread B builds: C โ†’ B โ†’ A (reversed)
// Combined result: A โ†’ B โ†’ A (circular reference)
// Next get() call on that bucket โ†’ infinite loop โ†’ 100% CPU usage

// Java 8 fixed this specific issue by using a different resize algorithm
// But HashMap is still NOT thread-safe in Java 8+
// Other race conditions remain

Problem 3 โ€” ConcurrentModificationException

// HashMap has a modCount field that increments on every structural change
// The iterator checks modCount at every step
// If modCount changes during iteration โ€” ConcurrentModificationException

HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

// Thread 1 โ€” iterating
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey());
}

// Thread 2 โ€” modifying during Thread 1's iteration
map.put("d", 4); // โ†’ Thread 1 gets ConcurrentModificationException

// This is called fail-fast iteration
// HashMap's iterator detects concurrent modification and throws immediately

Important: ConcurrentModificationException can also happen in a single thread if you modify the map while iterating over it with a for-each loop. Always use iterator.remove() or collect keys to remove separately.

How ConcurrentHashMap Solves These Problems

ConcurrentHashMap was designed from the ground up for concurrent access. Its internal design is significantly different from HashMap.

Java 7 โ€” Segment-Based Locking

// Java 7 ConcurrentHashMap divided the map into 16 segments (by default)
// Each segment was an independent HashMap with its own ReentrantLock
// Threads could write to different segments simultaneously
// Only threads targeting the same segment competed for the lock
// Default: 16 threads could write concurrently (one per segment)

// Concurrency level = number of segments = 16 by default

Java 8 โ€” Node-Level (Bucket-Level) Locking

// Java 8 redesigned ConcurrentHashMap completely
// Instead of segment-level locking, it uses bucket-level (node-level) locking
// Each bucket head node acts as its own lock using synchronized
// Reads are completely lock-free โ€” uses volatile reads
// Only writes to the same bucket require synchronization

// This means:
// Multiple threads can READ simultaneously โ€” no locks at all
// Multiple threads can WRITE to different buckets โ€” simultaneously
// Only threads writing to the SAME bucket wait for each other

// Result: dramatically higher concurrency than the segment approach

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// Thread 1 โ€” writing to bucket 5
map.put("Randhir", 1); // locks only bucket 5

// Thread 2 โ€” writing to bucket 11 simultaneously
map.put("Priya", 2);   // locks only bucket 11 โ€” no conflict with Thread 1

// Thread 3 โ€” reading simultaneously
map.get("Randhir");    // completely lock-free โ€” volatile read

Fail-Safe Iterator

// ConcurrentHashMap uses a weakly consistent iterator
// It does NOT throw ConcurrentModificationException
// It reflects the state of the map at the time the iterator was created
// Modifications during iteration may or may not be reflected

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

// Safe to iterate and modify concurrently โ€” no exception thrown
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey());
    map.put("d", 4); // โœ… no ConcurrentModificationException
}

Code Example โ€” Using Both in Practice

// โŒ HashMap โ€” unsafe in multi-threaded environment
public class UnsafeCache {
    private Map<String, String> cache = new HashMap<>(); // NOT thread-safe

    public void store(String key, String value) {
        cache.put(key, value); // race condition if called from multiple threads
    }

    public String get(String key) {
        return cache.get(key); // may return stale or null even if key exists
    }
}

// โœ… ConcurrentHashMap โ€” safe for concurrent access
public class SafeCache {
    private Map<String, String> cache = new ConcurrentHashMap<>();

    public void store(String key, String value) {
        cache.put(key, value); // thread-safe, no external synchronization needed
    }

    public String get(String key) {
        return cache.get(key); // always returns consistent value
    }
}

// โœ… Even better โ€” use atomic operations to avoid race conditions
public class BetterCache {
    private Map<String, String> cache = new ConcurrentHashMap<>();

    public String getOrCreate(String key, String defaultValue) {
        // putIfAbsent is atomic โ€” no race condition between check and insert
        cache.putIfAbsent(key, defaultValue);
        return cache.get(key);
    }

    // computeIfAbsent โ€” atomic and even cleaner
    public String getOrCompute(String key) {
        return cache.computeIfAbsent(key, k -> expensiveComputation(k));
    }
}

Atomic Operations in ConcurrentHashMap

One of the biggest advantages of ConcurrentHashMap over a synchronized HashMap is its set of built-in atomic operations. These allow you to perform check-then-act operations safely without external synchronization.

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// putIfAbsent โ€” put only if key does not exist (atomic)
map.putIfAbsent("visits", 0);

// โŒ NOT atomic โ€” race condition between get and put
if (!map.containsKey("visits")) {
    map.put("visits", 0); // another thread might insert between these two lines
}

// computeIfAbsent โ€” compute value only if key is absent (atomic)
map.computeIfAbsent("userId_123", key -> loadFromDatabase(key));

// computeIfPresent โ€” update value only if key exists (atomic)
map.computeIfPresent("visits", (key, oldValue) -> oldValue + 1);

// compute โ€” always update, regardless of existing value (atomic)
map.compute("visits", (key, oldValue) -> oldValue == null ? 1 : oldValue + 1);

// merge โ€” merge new value with existing (atomic)
map.merge("visits", 1, Integer::sum);
// If "visits" does not exist โ†’ sets it to 1
// If "visits" exists โ†’ adds 1 to existing value

// Real use case โ€” thread-safe visit counter
public void recordVisit(String pageSlug) {
    visitCounts.merge(pageSlug, 1, Integer::sum);
}

ConcurrentHashMap vs Other Thread-Safe Alternatives

// Option 1: Collections.synchronizedMap() โ€” wraps HashMap with a mutex
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// โŒ Locks the ENTIRE map for every read AND write
// โŒ Must manually synchronize during iteration
// Performance: poor under high concurrency

synchronized (syncMap) { // must synchronize iteration manually
    for (Map.Entry<String, String> entry : syncMap.entrySet()) {
        System.out.println(entry);
    }
}

// Option 2: Hashtable โ€” legacy, avoid in new code
Hashtable<String, String> table = new Hashtable<>();
// โŒ Synchronizes every single method on the entire object
// โŒ No null keys or values
// โŒ Very poor performance
// Only kept for backward compatibility

// Option 3: ConcurrentHashMap โ€” recommended
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// โœ… Bucket-level locking โ€” much higher concurrency
// โœ… Lock-free reads
// โœ… Atomic composite operations
// โœ… Fail-safe iterator

// Performance comparison under high concurrency:
// Hashtable           โ†’ very slow (one lock for everything)
// synchronizedMap     โ†’ slow (one lock for everything)
// ConcurrentHashMap   โ†’ fast (lock per bucket, lock-free reads)

When to Use HashMap vs ConcurrentHashMap

// Use HashMap when:
// - Single-threaded code
// - You need null keys or null values
// - The map is created and populated once, then only read (effectively immutable)
// - Performance in single-threaded context is critical

// Examples of safe HashMap use:
public List<String> processData(List<String> items) {
    Map<String, Integer> frequency = new HashMap<>(); // local variable โ€” thread-safe
    for (String item : items) {
        frequency.merge(item, 1, Integer::sum);
    }
    return new ArrayList<>(frequency.keySet());
}

// Use ConcurrentHashMap when:
// - Multiple threads read and write the map
// - You need a shared cache across requests
// - You need atomic composite operations (computeIfAbsent, merge)
// - You are storing session data, rate limits, or counters

// Examples of necessary ConcurrentHashMap use:
@Component
public class RateLimiter {
    // Shared across all incoming HTTP requests โ€” MUST be ConcurrentHashMap
    private final Map<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();

    public boolean isAllowed(String clientIp) {
        AtomicInteger count = requestCounts.computeIfAbsent(
            clientIp,
            ip -> new AtomicInteger(0)
        );
        return count.incrementAndGet() <= 100; // max 100 requests
    }
}

@Service
public class SessionStore {
    // Multiple threads (one per request) access this simultaneously
    private final Map<String, UserSession> sessions = new ConcurrentHashMap<>();

    public void createSession(String token, UserSession session) {
        sessions.put(token, session);
    }

    public UserSession getSession(String token) {
        return sessions.get(token);
    }

    public void invalidate(String token) {
        sessions.remove(token);
    }
}

Common Mistakes to Avoid

1. Using HashMap in a Spring Bean

// โŒ Spring beans are singletons โ€” shared across all requests
@Service
public class ProductService {
    // This HashMap is shared between ALL threads handling requests
    private Map<String, Product> cache = new HashMap<>(); // DANGEROUS

    public Product getProduct(String id) {
        if (!cache.containsKey(id)) {
            cache.put(id, loadFromDb(id)); // race condition
        }
        return cache.get(id);
    }
}

// โœ… Fix โ€” use ConcurrentHashMap with atomic computeIfAbsent
@Service
public class ProductService {
    private Map<String, Product> cache = new ConcurrentHashMap<>();

    public Product getProduct(String id) {
        return cache.computeIfAbsent(id, this::loadFromDb); // atomic
    }
}

2. Compound operations on ConcurrentHashMap without using atomic methods

// โŒ Not atomic โ€” race condition between containsKey and put
if (!map.containsKey(key)) {
    map.put(key, value); // another thread might insert between these two lines
}

// โœ… Atomic โ€” use putIfAbsent
map.putIfAbsent(key, value);

// โŒ Not atomic โ€” race condition between get and put
Integer count = map.get(key);
map.put(key, count == null ? 1 : count + 1);

// โœ… Atomic โ€” use merge
map.merge(key, 1, Integer::sum);

3. Expecting ConcurrentHashMap to guarantee ordering

// Neither HashMap nor ConcurrentHashMap guarantee insertion order
// If you need order โ€” use LinkedHashMap (single thread) 
// or wrap with Collections.synchronizedMap (multi thread, with poor performance)

// For sorted order โ€” use TreeMap (single thread)
// or ConcurrentSkipListMap (multi thread, sorted, excellent concurrency)

ConcurrentSkipListMap<String, Integer> sortedConcurrent = new ConcurrentSkipListMap<>();
// Thread-safe AND sorted by key โ€” the concurrent alternative to TreeMap

Final Thought

The choice between HashMap and ConcurrentHashMap is straightforward once you understand the context: if your map is accessed by more than one thread โ€” even occasionally โ€” use ConcurrentHashMap. The performance overhead is minimal compared to the correctness risk of a HashMap in a concurrent context.

In Java web applications, where every incoming request runs in its own thread and Spring beans are singletons by default, any instance-level map field should almost always be a ConcurrentHashMap. HashMap belongs in local method scope where it is not shared between threads.

Also remember โ€” ConcurrentHashMap makes individual operations thread-safe, but compound operations (check-then-act, read-modify-write) still require the atomic methods it provides. Use putIfAbsent, computeIfAbsent, compute, and merge instead of combining separate get and put calls.

For more Java concurrency content check out our guide on Java Interview Questions for 6+ Years Experience which covers thread safety, volatile, atomic variables, and more in depth.

Head First Java, Third Edition
โœฆ Developer Pick

Head First Java, Third Edition

Ready to learn Java? This book combines puzzles, strong visuals, mysteries, and soul-searching interviews with famous Java objects to engage you in many different ways.

Subscribe to Our Newsletter

Join Our Developer Community!

Unsubscribe Anytime | No Spam