ConcurrentHashMap Deep Dive: Understanding Java’s Core Concurrent Container
This article explains why HashMap isn’t thread‑safe, how Hashtable’s single lock hurts performance, and walks through the evolution from JDK 1.7 segment locks to JDK 1.8’s CAS‑plus‑synchronized bucket locking, including lock‑free size counting and a comparison with CopyOnWriteArrayList for read‑heavy scenarios.
After covering locks and thread pools, this piece examines the concurrent container that almost every Java backend project relies on— ConcurrentHashMap. It appears in local caches, counters, sliding‑window rate limiters, and even thread‑pool task tracking.
HashMap vs. Hashtable
HashMapis not thread‑safe; concurrent put can corrupt data, and during resize in JDK 7 a linked‑list may form a cycle, causing get to loop forever and CPU to spike. JDK 8 changed the resize algorithm to tail‑insertion, avoiding cycles but still not thread‑safe. Hashtable guarantees safety by wrapping every method with a single synchronized lock, effectively a global lock that serialises all accesses, turning concurrency into a bottleneck. ConcurrentHashMap aims to provide thread safety without the global lock, using two different designs in JDK 1.7 and JDK 1.8.
JDK 1.7: Segment Locks
The map maintains an array of Segment objects (default length 16, controlled by concurrencyLevel). Each Segment is a mini‑ Hashtable with its own ReentrantLock and a HashEntry array.
During put(key, value) the key is hashed to locate a specific Segment, then that segment’s lock is acquired and the entry is inserted into its internal hash table. The other 15 segments remain unlocked, allowing up to 16 concurrent writes when keys fall into different segments.
However, segment‑level locking has drawbacks: operations spanning multiple segments (e.g., size()) become complex, and the segment array size is fixed at creation, limiting scalability.
JDK 1.8: CAS + Synchronized Bucket Locking
JDK 1.8 discards Segment and reuses the classic HashMap bucket array ( Node[] table). Lock granularity is refined to a single bucket:
If a bucket is empty, a CAS operation inserts the new node without any lock.
If a bucket already contains nodes (hash collision), synchronized locks only the bucket’s head node, allowing other buckets to be accessed concurrently.
The implementation includes a simplified putVal core logic (shown below) that demonstrates the CAS‑first, fallback‑to‑synchronized approach.
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // initialize table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<>(hash, key, value)))
break; // empty bucket, CAS succeeded
} else if (onlyIfAbsent && f.hash == hash && ...)
return f.val;
else {
synchronized (f) { // bucket head locked
// insert or update in list/tree
}
}
}The shift from ReentrantLock to optimized synchronized (post‑JDK 6) yields comparable performance under low contention and eliminates manual unlock calls.
Lock‑Free Size Counting
In JDK 1.7, size() locks every segment and aggregates counts, effectively pausing concurrency. JDK 1.8 adopts a LongAdder -like strategy: a baseCount field plus a CounterCell[] array. Updates use CAS on baseCount; under high contention, increments are spread across cells, reducing contention. The returned size is an approximate snapshot, similar to LongAdder.sum(), trading exactness for throughput.
CopyOnWriteArrayList: A Different Philosophy
For scenarios with many reads and few writes (e.g., product white‑lists in flash‑sale systems), CopyOnWriteArrayList offers an alternative: writes acquire a ReentrantLock, copy the entire underlying array, modify it, then atomically replace the reference. Reads never lock and always see a consistent snapshot of the array at the moment the iterator was created.
This design yields zero‑cost reads but incurs high write overhead proportional to the list size and write frequency. Its iterator provides weak consistency: it never throws ConcurrentModificationException but may return stale data.
Choosing the Right Container
High‑concurrency read/write Map (cache, counters): ConcurrentHashMap – CAS + bucket‑level synchronized gives near‑lock‑free reads and fine‑grained writes.
Need a strong, consistent snapshot: Use a locked HashMap or accept the approximate nature of ConcurrentHashMap.size() and traversal.
Read‑heavy, write‑rare List (whitelists, configs): CopyOnWriteArrayList – reads are lock‑free; writes copy the array.
Write‑heavy List: Prefer a locked ArrayList or Collections.synchronizedList; CopyOnWriteArrayList would be too costly.
Just need a thread‑safe tag: Avoid legacy Hashtable or Vector – their global lock hurts performance.
The overarching theme is that concurrent containers have evolved by reducing lock granularity: from a single global lock in Hashtable, to segment locks in JDK 1.7, to bucket‑level CAS‑plus‑synchronized in JDK 1.8, while CopyOnWriteArrayList takes a completely different “space‑for‑time” approach.
Next up: blocking queues and the producer‑consumer model, covering BlockingQueue families and a brief look at the Disruptor design.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
