Java Concurrent Collections: Fine-Grained Locking and Cache-Line Striping
Java’s standard HashMap and ArrayList are not thread-safe. Wrapping them with Collections.synchronizedMap gives you a monitor-protected map, but a single lock serializes every operation — which means under concurrent write pressure, threads spend most of their time waiting, not working.
The JDK provides two families of concurrent collections that solve this problem, each with a fundamentally different strategy: ConcurrentHashMap uses fine-grained per-bin locking so writes to different keys can proceed in parallel. LongAdder uses spatial striping — multiple cache-line-separated cells — so threads hitting different addresses never contend at all.
ConcurrentHashMap: per-bin locking
ConcurrentHashMap does not use a single global lock. Instead, it divides the map into bins (buckets) and acquires a fine-grained lock on only the bin whose key is being modified. On Java 8+ this is done via synchronized on the head node of each bin’s linked list or tree, combined with CAS for structural mutations.
The result: if two threads update keys that hash to different bins, they proceed entirely in parallel — no spinning, no cache-line bouncing between cores.
Five hundred thousand concurrent merge() calls across two threads finished in 68 ms with a correct final count of 500,000. The computeIfAbsent call computed the mapping function only once (not 2,000 times) even though both threads raced to compute it simultaneously — ConcurrentHashMap guarantees exactly one computation per key.
Weakly consistent iteration is another differentiator: an iterator over the map does not throw ConcurrentModificationException when other threads modify the map concurrently. It reflects the map’s state as of some point during the iteration rather than snapshotting at a specific instant. This trade-off — eventually consistent reads instead of atomic snapshot reads — is what lets ConcurrentHashMap avoid holding locks during traversal.
The practical takeaway: if your workload has concurrent updates, lookups, and iterations all interleaved, ConcurrentHashMap gives you correctness without the throughput cliff that hits a synchronized map.
LongAdder: spatial striping for counters
AtomicLong uses a single volatile field with a CAS loop (compareAndSet) on every increment. That works fine when there’s little contention — but under heavy multi-core pressure, all threads fight over the same cache line, and every failed CAS forces a retry.
LongAdder avoids this entirely through striping. It maintains an array of cells (one per CPU core in practice), each padded to a cache line so that concurrent writes touch different memory locations. Each thread increments its own cell via CAS — no contention at all. The sum() method adds up all cells, plus a base value, on read.
At 8 threads × 500K increments each, LongAdder completed in 38 ms versus AtomicLong’s 59 ms — a 1.5× speedup. Both produced the correct total of 4,000,000. The gap widens with more cores because LongAdder’s cells scale with available hardware concurrency while AtomicLong’s single CAS target does not.
The trade-off: sum() is O(n_cells) rather than O(1), and in-between values during concurrent updates are approximate (you can only rely on the final sum). For high-frequency counters where reads are infrequent relative to writes, this is an excellent trade.
Takeaway
Concurrent collections avoid the single-lock bottleneck through two distinct strategies: ConcurrentHashMap spreads locks across bins so concurrent writes touch different memory regions, and LongAdder spreads state across cells so concurrent updates never contend. Both accept a modest consistency relaxation in exchange for throughput — ConcurrentHashMap iteration reflects some-but-not-all concurrent mutations, and LongAdder’s intermediate sums are approximate. Pick the one that matches your pattern: per-key mutable state or shared counters.