Stage 5: Collections compared, lesson 4 of 8

HashMap vs ConcurrentHashMap

Advanced3 min read@since 8Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

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, merge and replace do check-and-update as one step.
  • No null keys or values, so get() returning null always 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 a long.

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

HashMapConcurrentHashMap
Thread-safeNoYes
LockingNonePer bucket (CAS + synchronized on the first node)
ReadsNo locking (unsafe with writers)Lock-free and safe
null keys / valuesAllowedNot allowed
IteratorsFail-fast (throw CME)Weakly consistent (never throw CME)
Atomic compound opsNot atomicputIfAbsent, compute, merge are atomic
size() with concurrent writesWrong or corruptedAn estimate (mappingCount() for long)
Speed in one threadSlightly fasterSlightly slower
Java 7 designn/a16 "segments", each a small locked table

Example

Java
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

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.