ConcurrentHashMap Internals: Coordinating Updates Without One Global Lock
Understand how modern ConcurrentHashMap combines volatile reads, CAS, and narrow bin-level locking, and where its guarantees stop.
On this page
Key takeaways
- Modern ConcurrentHashMap does not use the old fixed set of lock segments; it coordinates updates at the bin level.
- Reads are generally non-blocking and rely on safely published table entries and values.
- Atomic per-key operations do not make a multi-key workflow transactional.
- Use compute methods for short map-local updates, not slow network or database calls.
The design goal
ConcurrentHashMap exists for a familiar workload: many threads need to read and update a shared mapping without serializing every operation behind one map-wide monitor. It is not a drop-in way to make every larger workflow thread-safe. Its contract protects the map's own operations and provides useful visibility guarantees, while callers remain responsible for coordinating invariants that span multiple keys or other objects.
The implementation changed substantially in Java 8. Older explanations often describe a fixed set of segments. Current implementations instead use a table of bins, with compare-and-set (CAS) for some initialization and update paths, and synchronization on a bin when a contended update needs coordination. Tree bins may be used for heavily collided buckets. The details are implementation-specific; the important point is that unrelated bins can usually progress independently.
Why reads can often avoid locking
The table and node fields are published with memory-ordering guarantees. A reader can inspect a bin safely while a writer coordinates a change, and a read does not normally acquire the lock used for that bin's update. This favors read-heavy workloads, but it does not promise that every compound observation is a consistent snapshot of the entire map.
For example, iterating while other threads modify the map is weakly consistent: the traversal can reflect some concurrent changes without throwing ConcurrentModificationException, but it is not a frozen point-in-time view. If a business operation needs a snapshot, create one under an appropriate application-level synchronization strategy or use a data model designed for snapshots.
Use atomic map operations narrowly
This pattern safely initializes a value for a key:
counts.computeIfAbsent("checkout", ignored -> new LongAdder()).increment();
The map coordinates the mapping operation, and LongAdder handles concurrent increments efficiently. Keep mapping functions short and side-effect-light. A mapping function that calls a remote service can hold up updates to the relevant bin, complicate retries, and make latency unpredictable. Perform expensive work outside the map operation, then publish a completed value with a clearly defined conflict policy.
Common traps
size()and aggregate methods are useful observations, not synchronization barriers for a changing map.putIfAbsentprotects one mapping decision; it does not protect related state held elsewhere.- Mutable values need their own concurrency strategy even when their reference is stored safely.
- A map-level atomic operation cannot replace a database transaction or a distributed lock.
Use ConcurrentHashMap when shared in-memory lookup and independent key updates are genuinely part of the design. If correctness depends on two keys changing together, make that invariant explicit rather than hoping a concurrent collection will provide it.