HashMap vs ConcurrentHashMap
HashMap is not thread-safe: concurrent writes can lose updates and corrupt its internals. ConcurrentHashMap is built for many threads:
- Fine-grained locking. In Java 8+, putting into an empty bucket uses a lock-free CAS operation; updating a non-empty bucket locks only that bucket (by synchronizing on its first node). Threads working on different buckets never wait for each other.
- Lock-free reads.
get()doesn't lock. - Atomic compound operations:
putIfAbsent,computeIfAbsent,compute,mergeandreplacedo check-and-update as one step. - No null keys or values, so
get()returningnullalways means "absent". - Weakly consistent iterators: no
ConcurrentModificationException; they may or may not show changes made during iteration. - Under concurrent updates,
size()is an estimate;mappingCount()returns along.
Use HashMap inside one thread (or when the map is never changed after being published safely), and ConcurrentHashMap whenever several threads read and write it.
Side by side
| HashMap | ConcurrentHashMap | |
|---|---|---|
| Thread-safe | No | Yes |
| Locking | None | Per bucket (CAS + synchronized on the first node) |
| Reads | No locking (unsafe with writers) | Lock-free and safe |
| null keys / values | Allowed | Not allowed |
| Iterators | Fail-fast (throw CME) | Weakly consistent (never throw CME) |
| Atomic compound ops | Not atomic | putIfAbsent, compute, merge are atomic |
| size() with concurrent writes | Wrong or corrupted | An estimate (mappingCount() for long) |
| Speed in one thread | Slightly faster | Slightly slower |
| Java 7 design | n/a | 16 "segments", each a small locked table |
Example
List<String> words = List.of("java", "spring", "java", "sql", "java", "spring");
Map<String, Integer> counts = new ConcurrentHashMap<>();
words.parallelStream().forEach(w -> counts.merge(w, 1, Integer::sum)); // atomic per key
System.out.println(counts); // {spring=2, java=3, sql=1} always correct
Map<String, Integer> broken = new HashMap<>();
words.parallelStream().forEach(w -> broken.merge(w, 1, Integer::sum)); // races: lost counts, corruption
// Lazily create shared objects exactly once per key:
Map<String, List<String>> byCourse = new ConcurrentHashMap<>();
byCourse.computeIfAbsent("spring", k -> new CopyOnWriteArrayList<>()).add("Asha");Common mistake
Writing if (!map.containsKey(k)) map.put(k, v) on a ConcurrentHashMap. Each call is safe, but the pair isn't; use putIfAbsent or computeIfAbsent.
Under the hood
Keep the functions you pass to computeIfAbsent and compute short, and never modify the same map from inside them: the bucket is locked while they run, so a slow function blocks other writers, and a recursive update can throw IllegalStateException. For counters under heavy contention, map.computeIfAbsent(k, x -> new LongAdder()).increment() scales even better than merge.
Check yourself
Which is an atomic way to increment a counter in a ConcurrentHashMap?
How this connects
Know these first
Where this leads
You've reached the end of this thread. Try a learning path for what's next.
Part of Multithreading: beginner to advanced, Java 8 and collections, practically.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.