Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,106visitors

Built for engineers who want to go deep.


← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools
  • Fork/Join Framework

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture
  • Deadlock, Starvation, Livelock

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
  • HashMap Concurrency Variants
  • Concurrent Utilities & Coordination
Chaturmind
← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools
  • Fork/Join Framework

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture
  • Deadlock, Starvation, Livelock

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
  • HashMap Concurrency Variants
  • Concurrent Utilities & Coordination
HomeLearnJavaJava Concurrency & MultithreadingConcurrent Collections
✓ FreeAdvanced· 13 min read

HashMap Concurrency Variants

Hashtable, synchronized HashMap, and ConcurrentHashMap before and after Java 8 internals — why lock granularity is the entire story, and where ConcurrentHashMap alone still isn't enough.

Published September 22, 2026


HashMap Concurrency Variants

Once HashMap Deep Dive is solid, the natural follow-up is: what do you use when multiple threads touch the map concurrently? Three real answers exist, and they differ almost entirely in lock granularity — how much of the map a thread blocks while it works.

Hashtable — full method-level synchronization

Map<String, Integer> map = new Hashtable<>();

Every public method on Hashtable is synchronized on the instance itself. A put() from thread A blocks a get() from thread B even if they touch completely unrelated keys — the entire map is one lock. This is why it's effectively obsolete: it gives you correctness at the cost of serializing all access, regardless of actual contention, which throws away most of the benefit of having multiple threads in the first place.

Advertisement

Collections.synchronizedMap() — same problem, different wrapper

Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());

This wraps a plain HashMap and synchronizes on a shared lock for every method call — functionally the same coarse-grained locking as Hashtable, just retrofittable onto any Map. Neither this nor Hashtable scale under real concurrent load; both exist mainly for legacy compatibility or trivially low-contention cases.

One shared trap with both: compound operations still aren't atomic even though each individual method call is synchronized.

if (!map.containsKey(k)) map.put(k, v); // NOT atomic — two separate synchronized calls

Between the containsKey and the put, another thread can insert the same key — this is exactly the gap ConcurrentHashMap.putIfAbsent() closes atomically.

ConcurrentHashMap, pre-Java 8: segment-based locking

Before Java 8, ConcurrentHashMap divided its internal table into 16 segments by default, each independently lockable. A put() only locks the single segment its key hashes into — up to 16 threads could write to different segments fully in parallel, a massive improvement over one lock for the whole map. Reads were mostly lock-free (volatile reads).

ConcurrentHashMap, Java 8+: CAS-based bin updates

Java 8 dropped segments entirely and moved to per-bin (per-bucket) synchronization, using CAS (compare-and-swap) operations for most updates:

  • Inserting into an empty bin: a lock-free CAS attempt — no synchronization block at all if it succeeds.
  • Inserting into a non-empty bin (a collision): synchronized only on that specific bin's first node — not the whole map, not even a whole segment, just that one bucket.

This pushed concurrency granularity from "16 segments" to effectively "one lock per bucket," which for a table with thousands of buckets means orders of magnitude less contention under real concurrent write load.

Why the read path is (mostly) lock-free

Reads (get()) don't synchronize at all in the common case — they rely on volatile reads of each Node's fields, guaranteeing visibility of the latest write without acquiring a lock. This is why ConcurrentHashMap reads scale almost linearly with thread count, in contrast to Hashtable, where even reads are serialized behind the single lock.

compute/computeIfAbsent — the atomicity guarantee that matters most

map.computeIfAbsent(key, k -> expensiveInitialization(k));

This is atomic per key — if two threads call computeIfAbsent for the same key simultaneously, the mapping function runs exactly once for that key, and both threads see the same resulting value. This closes the check-then-act gap that plagues Collections.synchronizedMap() (see above), and it's the correct tool for lazy-initialization-under-concurrency patterns (e.g. a per-key cache).

The trap: the mapping function must not attempt to modify the same map recursively (e.g. call map.put() on a different key from inside the lambda) — this can deadlock or throw IllegalStateException, since the bin is locked for the duration of the computation.

Where ConcurrentHashMap alone is NOT enough

Every individual operation on ConcurrentHashMap is thread-safe. Compound operations spanning multiple keys are not. Example: transferring a value between two keys —

Integer fromBalance = map.get("accountA");
map.put("accountA", fromBalance - amount);
map.put("accountB", map.get("accountB") + amount); // race window between these calls

Nothing in ConcurrentHashMap's contract protects multi-key invariants — another thread can observe or modify "accountB" in the gap between these lines. This needs external locking (a lock per logical transaction, or moving to a different concurrency-control strategy entirely, like optimistic locking with a version check) — ConcurrentHashMap solves per-key atomicity, not cross-key transactional consistency.

Follow-up questions this topic invites — and their answers

Q: Does ConcurrentHashMap allow null keys or values? A: No, unlike HashMap. This is deliberate: in a concurrent context, map.get(key) == null is ambiguous between "key absent" and "key present with null value," and resolving that ambiguity safely without a lock isn't possible — disallowing null sidesteps the whole problem.

Q: Is size() on ConcurrentHashMap exact? A: It's a best-effort estimate under concurrent modification — by the time size() returns, the map may have already changed. For genuinely exact counts under concurrency, you need a different design (e.g. an external atomic counter maintained alongside the map).

Q: Why did segments disappear in Java 8 instead of just tuning the segment count? A: Segments capped concurrency at a fixed number (16 by default) regardless of table size — a huge table still had only 16 lockable units. Per-bin locking scales the number of independently-lockable units with the table itself, which is strictly finer-grained and removes the fixed ceiling.

Q: When would you still reach for Collections.synchronizedMap() over ConcurrentHashMap today? A: Rarely — mainly when you need null keys/values and can tolerate coarse locking, or when wrapping an existing Map implementation that isn't HashMap and no concurrent equivalent exists for it.

Advertisement

Previous

ConcurrentHashMap & CopyOnWriteArrayList

Next

Concurrent Utilities & Coordination

AI Tutor

Lesson: HashMap Concurrency Variants

Quick actions

AI responses can be inaccurate. Verify critical information.