Understanding Java ConcurrentHashMap: Implementation, Locking, and Performance
This article explains the shortcomings of HashMap, compares Hashtable, synchronizedMap, and ConcurrentHashMap implementations in JDK 7 and JDK 8, discusses lock states, fail‑fast vs fail‑safe behavior, and provides code examples and optimization tips for thread‑safe map operations.
HashMap has two main drawbacks: it is not thread‑safe and its entries cannot be modified during iteration.
Hashtable
Hashtable synchronizes every method with a synchronized lock, causing all threads to contend for a single lock and reducing concurrency.
Collections#SynchronizedMap
The synchronized wrapper uses synchronized blocks but still does not improve concurrency significantly.
ConcurrentHashMap
ConcurrentHashMap’s implementation differs between JDK 7 and JDK 8.
JDK 7
It uses a segmented structure where each Segment (a ReentrantLock ) contains an array of HashEntry objects, forming an array‑plus‑linked‑list layout. Modifications acquire the segment lock, providing segment‑level (partial) locking.
JDK 8
It adopts the same array‑plus‑linked‑list‑plus‑red‑black‑tree structure as HashMap but employs finer‑grained locking: CAS operations and synchronized locks at the table‑bucket level, allowing only the first node of a bucket to be locked.
Why does JDK 8 still use synchronized locks instead of other lock types?
Because the synchronized keyword has been heavily optimized in recent JDKs (bias lock → lightweight lock → heavyweight lock), offering good performance and multiple lock states.
Lock States
No‑Lock : No locking; only one thread can successfully modify a resource at a time.
Biased Lock : A thread repeatedly accesses synchronized code, automatically acquiring the lock without extra cost.
Lightweight Lock : When another thread tries to acquire a biased lock, it spins instead of blocking.
Heavyweight Lock : After excessive spinning or contention, the lock inflates to a heavyweight monitor.
ConcurrentHashMap’s get method is lock‑free and safe because the stored value is volatile , guaranteeing visibility under the Java Memory Model.
Why can ConcurrentHashMap’s key and value not be null ?
Because a null return from get(k) would be ambiguous—whether the key is absent or the value is explicitly null .
Explain fail‑fast vs fail‑safe in Java collections.
Fail‑fast iterators detect concurrent modifications via a modCount check and throw ConcurrentModificationException . Fail‑safe collections (e.g., those in java.util.concurrent ) work on a snapshot copy, so modifications do not affect the iterator.
ConcurrentHashMap put Logic
JDK 7
Attempt to acquire the segment lock (spinning up to 64 times, then blocking).
Locate the bucket via the key’s hashcode.
Traverse the linked HashEntry chain: replace the value if the key matches, otherwise create a new entry and possibly trigger resizing.
Release the segment lock.
JDK 8
Locate the node (bucket). If it is null , insert using CAS.
If the bucket is being resized ( first.hash == MOVED ), help with the resize.
If the bucket is a normal list, synchronize on the first node, determine whether it is a list or a tree, and insert accordingly.
How does ConcurrentHashMap compute its size?
JDK 7
It sums the count of each segment. If the sum changes during the attempt, it retries with a lock‑protected aggregation.
JDK 8
It uses a baseCount (updated via CAS) and an array of counterCells to accumulate counts without heavy locking.
How to improve ConcurrentHashMap insertion efficiency?
Reduce resizing by configuring appropriate initial capacity and load factor, and minimize lock contention by using the spread method to distribute keys evenly, keeping most operations at the biased‑lock level.
Is ConcurrentHashMap ever thread‑unsafe?
Yes, when a sequence of get , compute, and put is performed without atomicity. Example:
public class ConcurrentHashMapNotSafeDemo implements Runnable {
private static ConcurrentHashMap
scores = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException {
scores.put("John", 0);
Thread t1 = new Thread(new ConcurrentHashMapNotSafeDemo());
Thread t2 = new Thread(new ConcurrentHashMapNotSafeDemo());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(scores);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Integer score = scores.get("John");
Integer newScore = score + 1;
scores.put("John", newScore);
}
}
}The output may be {John=1323} , showing lost updates.
Solution: use the atomic replace method (or compute / merge ) which updates only if the expected old value matches, ensuring thread safety.
Source: blog.csdn.net/a979331856/article/details/105922069
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.