HashMap vs ConcurrentHashMap in Java - Complete Guide with Examples
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
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.
๐ You Might Also Like
- โ Best Google AdSense Alternative 2026 - Monetag Review for Publishers miscellaneous
- โ Top Java Interview Questions for 6+ Years Experience (2026) java
- โ HTTP QUERY Method (RFC 10008) โ The New HTTP Method Every Developer Should Know miscellaneous
- โ How to Start Freelancing as a Web Developer in 2026 (Complete Beginner's Guide) miscellaneous
- โ MyLync - Best Free Linktree Alternative for Developers, Creators & Businesses miscellaneous