What’s the difference between HashMap, Hashtable, and ConcurrentHashMap, and when would you use each?
HashMap allows one null key and multiple null values, is not synchronized, and simply breaks (throws ConcurrentModificationException or produces incorrect results) if two threads write to it at the same time. Hashtable is synchronized on every method with a single lock covering the whole map, which makes it thread-safe but serializes every read and write, so it doesn’t scale under concurrent load. ConcurrentHashMap is the one actually used in production multi-threaded code: instead of one lock for the whole map, it splits the map into segments (in modern JDKs, it uses fine-grained per-bucket locking with CAS operations), so multiple threads can read and write different parts of the map at the same time without blocking each other.
When to use which
Use HashMap whenever the map is only touched by one thread, which is the majority of cases (local variables, request-scoped data). Use ConcurrentHashMap the moment the map is shared across threads, for example an in-memory cache shared by request handlers. Reach for Hashtable essentially never in new code, it’s a legacy class kept around for backward compatibility; if you see it in a codebase, that’s usually a sign the code predates ConcurrentHashMap (introduced in Java 5) and is a candidate for modernization.
Edge case interviewers probe for
Ask what “thread-safe” actually guarantees here: ConcurrentHashMap guarantees the map’s internal structure won’t corrupt under concurrent access, but it does NOT make compound operations like “check if key exists, then insert” atomic unless you use the specific atomic methods (putIfAbsent, computeIfAbsent, merge). A candidate who says “ConcurrentHashMap is just thread-safe HashMap” and stops there hasn’t actually used it under real concurrency.
Common mistake
Wrapping a HashMap with Collections.synchronizedMap() and assuming it’s equivalent to ConcurrentHashMap. It’s thread-safe for individual operations, but every method call locks the entire map (similar to Hashtable), and iterating over it still requires manual synchronization on the map object to avoid ConcurrentModificationException. It’s a valid fix for legacy code but strictly worse than ConcurrentHashMap for anything performance-sensitive.
What the interviewer is checking
Whether you understand that “thread-safe” is not one single guarantee, it has degrees (whole-map locking vs. fine-grained locking vs. atomic compound operations), and whether you can match the right tool to actual concurrency requirements instead of reaching for the first “thread-safe” class you remember.
Imagine a shared filing cabinet that multiple office workers need to use. A HashMap is a cabinet with no rules: if two people yank open the same drawer at once, papers get mixed up or lost. Fine if only one person ever uses it, dangerous the moment a second person shows up.
A Hashtable is the same cabinet but with one giant lock on the entire thing: only one person can touch it at all, even if they want completely different drawers. Safe, but everyone else just stands in line waiting, even if their business has nothing to do with each other’s drawer.
A ConcurrentHashMap is a smarter cabinet: each drawer has its own small lock. Two people can open different drawers at the exact same time without waiting on each other, and the cabinet only makes someone wait if they truly need the same drawer someone else is using right now. That’s why it’s fast even with lots of people using it.
Why interviewers ask this
Almost every Java codebase eventually shares state across threads (caches, counters, connection pools). This question filters for people who understand concurrency at more than a buzzword level.
What a strong answer signals
That you know synchronization has a performance cost, and that “thread-safe” isn’t binary, some approaches serialize everything, others allow real parallelism.
Common follow-ups
- How does ConcurrentHashMap achieve thread safety internally?
- Is
size()on a ConcurrentHashMap always exact? - How would you atomically increment a counter stored as a map value?
Advanced variation
“Design a thread-safe in-memory cache with expiry,” which expects you to combine ConcurrentHashMap with a scheduled cleanup mechanism or a library like Caffeine, rather than hand-rolling locks.
A common production pattern: an API gateway keeps a local in-memory cache of rate-limit counters per client, read and updated by every incoming request thread. Using a plain HashMap here caused sporadic ConcurrentModificationException crashes under load testing. Switching to ConcurrentHashMap with compute() for the increment-and-check logic eliminated the crashes and, because different clients’ counters live in different internal segments, throughput stayed high even at several thousand requests per second.
public class RateLimiter {
// Safe for many threads to share; each key's bucket updates independently.
private final ConcurrentHashMap<String, Integer> requestCounts = new ConcurrentHashMap<>();
private static final int LIMIT_PER_MINUTE = 100;
public boolean allowRequest(String clientId) {
// computeIfAbsent + merge are atomic: no lost updates under concurrent calls.
int count = requestCounts.merge(clientId, 1, Integer::sum);
return count <= LIMIT_PER_MINUTE;
}
public void resetWindow() {
requestCounts.clear();
}
}- 1HashMap is fast but not thread-safe; use it only when a single thread owns the map.
- 2Hashtable is thread-safe but locks the whole map, so it doesn’t scale under concurrency.
- 3ConcurrentHashMap uses fine-grained locking, letting different threads work on different parts of the map at once.
- 4Compound operations still need atomic methods like putIfAbsent, computeIfAbsent, or merge, not manual check-then-act.
- 5Avoid Hashtable in new code; it exists only for backward compatibility.