ConcurrentHashMap is the thread-safe hash map of the java.util.concurrent package, built so that many threads can read and update it at the same time without locking the whole map. Reads take no lock at all, and an update locks at most the one bucket it changes.
We use ConcurrentHashMap for shared caches, counters per key, session or connection registries and any lookup table that several threads change while the app runs. Its atomic methods, such as merge() and computeIfAbsent(), replace the code with separate get() and put() calls that breaks under concurrency.
The following example shows the operations this guide covers, with the result of each call as a comment.
ConcurrentHashMap<String, Integer> views = new ConcurrentHashMap<>();
views.put("home", 1);
Integer existing = views.putIfAbsent("home", 5); // 1, no change
Integer homeViews = views.merge("home", 1, Integer::sum); // 2
Integer cartViews = views.computeIfAbsent("cart", k -> 0); // 0
Integer cartAfter = views.compute("cart", (k, v) -> v + 10); // 10
boolean replaced = views.replace("cart", 10, 11); // true
Integer helpViews = views.getOrDefault("help", 0); // 0
long entries = views.mappingCount(); // 2
Integer total = views.reduceValues(1, Integer::sum); // 13
Notice that every update method returns the value it left in the map, so we never need a separate get(). We start with how the map works since Java 8, cover the atomic methods, caching, null rules, iteration, bulk operations, concurrent sets and sizing, and finish with the mistakes that show up most often in code reviews.
1. How ConcurrentHashMap Works Since Java 8
The map keeps its entries in one array of buckets, also called bins. The array is created on the first insert, not in the constructor. Each bin holds either nothing, a short linked list of nodes or, for crowded bins, a balanced tree.

- A get() takes no lock. The node fields it reads are volatile, so it sees every update that finished before the read started.
- A put() into an empty bin uses a compare-and-swap (CAS) instruction, which sets the slot only if it is still empty, so no lock is needed.
- A put() into a bin that already has nodes locks the first node of that bin with synchronized. Updates to other bins go ahead at the same time.
- When a bin reaches 8 nodes and the table has at least 64 bins, the bin turns into a red-black tree, so lookups stay fast even with poor hash codes.
- When the table grows, threads that call put() during the resize help move bins to the new array instead of waiting.
Java 7 and earlier split the map into 16 segments, each with its own lock. JEP 155 replaced that design in Java 8 with the per-bin scheme above, so articles that describe segments describe a map that no longer exists.
2. Atomic Methods for Compound Updates
Each single call on ConcurrentHashMap is thread safe, but two calls in a row are not one atomic step. Code that reads a value with get() and writes a new one with put() lets another thread write in between, and its update is lost.
Say a web app counts page views per URL path from all request threads. The following example runs a version with separate get() and put() calls and the merge() version from two threads each.
ConcurrentHashMap<String, Integer> pageViews = new ConcurrentHashMap<>();
ConcurrentHashMap<String, Integer> safeViews = new ConcurrentHashMap<>();
Runnable visit = () -> {
for (int i = 0; i < 100_000; i++) {
Integer current = pageViews.get("home"); // check
pageViews.put("home", current == null ? 1 : current + 1); // then act
safeViews.merge("home", 1, Integer::sum); // one atomic step
}
};
Thread visitor1 = Thread.ofPlatform().start(visit);
Thread visitor2 = Thread.ofPlatform().start(visit);
visitor1.join();
visitor2.join();
int racyCount = pageViews.get("home"); // less than 200000 in most runs
int mergedCount = safeViews.get("home"); // 200000
Any read followed by a write on the same key belongs in one of the atomic methods, never in separate get() and put() calls. The whole method call, including the function we pass, runs atomically for that key.
| Method | What it does atomically |
|---|---|
| putIfAbsent(k, v) | Adds the entry only if the key is missing, returns the current value |
| computeIfAbsent(k, fn) | Creates the value with fn only if the key is missing |
| computeIfPresent(k, fn) | Replaces or removes (when fn returns null) an existing value |
| compute(k, fn) | Computes a new value from the key and the old value, or null to remove |
| merge(k, v, fn) | Puts v if absent, otherwise combines the old value and v with fn |
| replace(k, old, new) | Replaces only if the current value equals old |
| remove(k, v) | Removes only if the current value equals v |
While the function runs, other threads that update the same bin wait. So the function must be short, must not call slow services, and must not change other keys of the same map, which can throw IllegalStateException with the message “Recursive update”. The ConcurrentMap guide compares these methods across both concurrent map classes.
3. Caching Expensive Values With computeIfAbsent()
The method computeIfAbsent() is the standard way to build a lazy cache. For a missing key it calls the loader once, stores the result and returns it. Other threads that ask for the same key at the same moment wait for that result instead of loading it again.
class PriceCache {
private final ConcurrentHashMap<String, Double> prices = new ConcurrentHashMap<>();
private final AtomicInteger loads = new AtomicInteger();
double price(String product) {
return prices.computeIfAbsent(product, this::loadPrice);
}
private double loadPrice(String product) {
loads.incrementAndGet(); // stands in for a database call
return product.length() * 1.5;
}
int loads() {
return loads.get();
}
}
PriceCache cache = new PriceCache();
try (ExecutorService shoppers = Executors.newFixedThreadPool(4)) {
for (int i = 0; i < 1_000; i++) {
shoppers.submit(() -> cache.price("apple"));
}
}
double applePrice = cache.price("apple"); // 7.5
int loaderCalls = cache.loads(); // 1
A thousand lookups from four threads load the price once. The trade-off is that a slow loader blocks other writers in the same bin while it runs. When loading takes seconds, we cache a CompletableFuture as the value, so computeIfAbsent() only starts the load and returns at once.
4. Null Keys and Null Values Are Not Allowed
Unlike HashMap, ConcurrentHashMap rejects null as a key and as a value. In a concurrent map, a null from get() must mean that the key is absent, because no lock lets us check with containsKey() before another thread changes the map.
ConcurrentHashMap<String, Integer> stock = new ConcurrentHashMap<>();
Integer missing = stock.get("kiwi"); // null, key is absent
Integer withDefault = stock.getOrDefault("kiwi", 0); // 0
Integer nullValue = stock.put("kiwi", null); // NullPointerException
Integer nullKey = stock.put(null, 5); // NullPointerException
When a value can be legitimately unknown, we store an Optional, a sentinel object or a special value, or we remove the key.
5. Iteration, size() and mappingCount()
The iterators of ConcurrentHashMap are weakly consistent. They never throw ConcurrentModificationException, they return each entry at most once, and they may or may not show changes made after the iteration started. So we can remove entries while we loop over the map.
ConcurrentHashMap<String, Integer> carts = new ConcurrentHashMap<>(Map.of("anna", 0, "ben", 2, "carl", 0));
for (Map.Entry<String, Integer> cart : carts.entrySet()) {
if (cart.getValue() == 0) {
carts.remove(cart.getKey()); // no ConcurrentModificationException
}
}
int activeCarts = carts.size(); // 1
The size() method returns an int, whereas mappingCount() returns a long and is the recommended method for large maps. Both return an estimate while other threads add or remove entries, so we never use the size to decide whether a key exists.
6. Bulk Operations With a Parallelism Threshold
Since Java 8, the map has its own forEach(), search() and reduce() methods that can split the work across the common ForkJoinPool. The first argument is a parallelism threshold, the number of entries above which the work runs in parallel.
- A threshold of Long.MAX_VALUE runs the operation sequentially in the calling thread.
- A threshold of 1 uses as much parallelism as the pool offers.
- A value in between, such as 10,000, runs in parallel only for maps larger than that.
ConcurrentHashMap<String, Integer> inventory = new ConcurrentHashMap<>(Map.of("apple", 5, "banana", 3, "cherry", 12));
Integer totalUnits = inventory.reduceValues(1, Integer::sum); // 20
Integer largest = inventory.reduceValues(Long.MAX_VALUE, Integer::max); // 12
String lowStock = inventory.search(1, (fruit, units) -> units < 4 ? fruit : null); // "banana"
long fruitKinds = inventory.mappingCount(); // 3
The search() method stops as soon as the function returns a non-null result. The bulk methods do not freeze the map, so on a map that other threads change at the same time, the result reflects some mix of old and new entries.
7. A Concurrent Set With newKeySet()
The JDK has no ConcurrentHashSet class. The static method ConcurrentHashMap.newKeySet() returns a thread-safe Set backed by a ConcurrentHashMap, with the same lock-free reads.
Set<String> onlineUsers = ConcurrentHashMap.newKeySet();
boolean added = onlineUsers.add("lokesh"); // true
boolean addedAgain = onlineUsers.add("lokesh"); // false
boolean removed = onlineUsers.remove("lokesh"); // true
The instance method keySet(defaultValue) returns a key view of an existing map that also supports add(), which puts the key with the default value.
ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>();
Set<String> players = scores.keySet(0);
players.add("alex");
Integer alexScore = scores.get("alex"); // 0
8. Sizing the Map and the concurrencyLevel Argument
The constructor argument initialCapacity is the number of entries the map should hold without resizing, so new ConcurrentHashMap<>(1_000) fits 1,000 entries before it grows. Presizing helps when we know the size up front, because each resize copies all bins.
The three-argument constructor still accepts loadFactor and concurrencyLevel, but since Java 8 both are sizing hints only. In the JDK 25 source, the constructor raises initialCapacity to concurrencyLevel if it is smaller and uses the load factor once to compute the first table size. After that, the map always resizes at 75% load.
| Constructor call | Effect in Java 8 and later |
|---|---|
| new ConcurrentHashMap<>() | Empty map, table of 16 bins created on first insert |
| new ConcurrentHashMap<>(1_000) | Table sized for 1,000 entries without a resize |
| new ConcurrentHashMap<>(16, 0.9f, 1) | Table sized for 16 entries at 0.9 density; no segments, no extra objects |
| new ConcurrentHashMap<>(16, 0.75f, 64) | Same as an initial capacity of 64 |
Tutorials written for Java 7 advise setting concurrencyLevel to 1 to avoid creating 16 segments. That advice applies to Java 7 only, and current code needs neither the argument nor the workaround.
9. Common ConcurrentHashMap Mistakes
Most bugs with ConcurrentHashMap come from treating a thread-safe map as if it made all code around it thread safe. The map protects its own structure and each single call, nothing more.
- Using separate containsKey(), get() and put() calls for one logical update, as in section 2.
- Storing mutable values such as an ArrayList and changing them after get(). The map does not protect the value object.
- Running slow I/O or updates to other keys inside compute() or computeIfAbsent().
- Deciding logic on size() or isEmpty() while other threads change the map.
- Expecting an order. For sorted keys we use ConcurrentSkipListMap.
The mutable-value mistake is common with maps of lists. The fix is a thread-safe value type, such as a concurrent set from newKeySet(), created inside computeIfAbsent().
ConcurrentHashMap<String, Set<String>> tagsByPost = new ConcurrentHashMap<>();
tagsByPost.computeIfAbsent("post-1", k -> ConcurrentHashMap.newKeySet()).add("java");
tagsByPost.computeIfAbsent("post-1", k -> ConcurrentHashMap.newKeySet()).add("threads");
int tagCount = tagsByPost.get("post-1").size(); // 2
When the code is single-threaded, a plain HashMap is simpler and faster. The synchronized HashMap comparison explains when Collections.synchronizedMap() is enough, and the Java concurrency guide lists the other concurrent collections.
10. ConcurrentHashMap FAQs
Most of these questions start from tutorials written for Java 7, when the map still had segments.
10.1. Are there still segments inside a ConcurrentHashMap?
No. Since Java 8, the map uses one table with CAS for empty bins and a lock on the first node of each non-empty bin. A Segment class still exists in the source, but only to keep old serialized maps readable.
10.2. Is ConcurrentHashMap completely thread safe?
Each method call is thread safe and the map never gets corrupted. A sequence of calls is not atomic, so compound updates must use compute(), merge() or the other atomic methods.
10.3. Does ConcurrentHashMap lock on reads?
No. The get() method and iteration take no locks, so reads never wait for writers. A read returns the result of the most recently completed update for that key.
10.4. Can we use ConcurrentHashMap with virtual threads?
Yes. The map blocks only for the short time an update holds a bin lock, and since Java 24 a virtual thread that blocks on synchronized no longer pins its carrier thread (JEP 491).
11. Conclusion
ConcurrentHashMap gives lock-free reads and per-bin locking for writes, which is why it scales far better than a map behind one lock. Since Java 8, its design has no segments, and concurrencyLevel is only a sizing hint.
We get correct results by putting every read and write of a key into merge(), compute() or computeIfAbsent(), keeping those functions short, and storing immutable or thread-safe values. The bulk methods, newKeySet() and mappingCount() cover aggregation, sets and size for large maps.
12. References
- ConcurrentHashMap (Java SE 25 API)
- ConcurrentMap (Java SE 25 API)
- JEP 155, Concurrency Updates
- java.util.concurrent package summary
Happy Learning !!
Hi ,
Can you explain internal working if it is possible
?
My question is suppose a iteration is going on the list as its read there is no lock. In between a write is done. Now control comes back to iteration so i think now concurrentModification will be thrown. Could you please explain how above scenario is handled. Is there any way to replicate such scenario?
Modification is being done on the copy of original map so the user will be able to see the original map that was present before current iteration. As soon as iteration completes the new map will be available for read purpose.
Hi lokesh,
Nice articles. Thanks for sharing n making our work easier. Could you please explain what is meaning of volatile read and what is its internal working?
Irrelevant?
When looking at the constructors of ConcurrentHashMap, the concurrencyLevel parameter does not do much, only to adjust the initialcapacity parameter if it is smaller than concurlevel.
Knowing these things become relevant when you go for performance improvement of application/ or avoid future performance issues, specially in high volume application.
Hi,
Thanks for this amazing blog, concepts are very well explained. When you get a chance could you please answer my question –
1. Say for example I have multiple threads (count = 8) that would add or update my Hashmap. What would be concurrencyLevel you would recommend?
2.
Won’t they have almost same performance since ConcurrentHashMap has only 1 Segment and all Threads will have to wait while updating HashEntry?
+1, also HashTable would give the same performance.
Article contains important details which are rarely explained on this subject. Thanks Lokesh really helped.
Thanks, nice post
what is meant by dense packaging inside ConcurrentHashMap ?
Load factor is the ratio between the number of “buckets” in the map and the number of expected elements. A value of 0.75 will suggest that if the buckets are more than 75% full, the Map should be resized. Similarly, 0.90 will suggest that if the buckets are more than 90% full, the Map should be resized.
This clarified my doubt, Thanks Lokesh,Vishal for asking it.
I think you are wrong regarding your load factor explanation.
If hashCode always returns 1, which means all entries will go to 1 bucket only. Do you mean the map will never resize?
You are right. Rehashing may never occur in such if load factor
>=1. But it will lead to very high number of collisions, badly impacting the performance.So the correct definition of load factor should be
the ratio between
1) the number of “Entries”
2) initialCapacity
not as you said
1) the number of “buckets” in the map
2) the number of expected elements.
Please be really cautious with basic knowledge of HashMap.
Thanks for clarification.
Nice article. But I would suggest one small correction
“First two are fairly simple as their name implies but last one is tricky part. This denotes the number of shards. ”
The parameter concurrencyLevel isn’t always equal to the number of segments created. It will only be true if it is a power of 2.
True. This parameter should be in power of two.
What is the bucket in HashMap ?
is it the Entry Array ? I am looking for collision condition solution when we have same hash code for 2 objcets.
In case of collision , objects are stored in same bucket and retrieved using equals function to get exact object.
Yes. Bucket is single index location in Entry array. If entry array is of size 10, the there are 10 buckets. Regarding conflict, I have written about internal working of HashMap. Please refer to linked post.
Hi,
I have doubt over the concurrencyLevel parameter.
Suppose I have initialized my ConcurrentHashMap with concurrencyLevel of 2.
I have 2 threads trying to update the first shard elements @ same time.
How this situation will be handled?
Do we need to apply extra care and synchronization for such cases ? Or it is handled internally by ConcurrentHashMap itself ?
No need to add extra synchronization. ConcurrentHashMap uses locks in put method.
https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/concurrent/ConcurrentHashMap.java
Hi,
No doubt it’s a wonderfull post. But I have two questions.
1) Is Shards and Segments are same?
2) What is the use of Segment
1) Yes, both are same.
2) Segments are to create different “groups of key-value pairs” so that different groups can be used used concurrently by different threads.
Is that means that each segments contains same key-value pair? Secondly if I create 20 shards then it means that 20 threads can do concurrent transaction on my map. Correct me if I am wrong.
Is that possible for you to explain that if I enter a key value pair in the concurrent hash map how it’s getting updated in multiple segments.
NO. A key-value pair inserted in ConcurrentHashMap will be stored in one-and-only-one segment. Let say you store 200 key-value pairs in ConcurrentHashMap having 20 segments, and if they are distributed equally the each segment will have 10 key-value pairs. Now these 20 shards are available to be concurrently accessed by 20 different threads.
If you try to lookup for value for any key, then first suitable segment is located and then suitable entry is located inside segment.
As I understand from the above one that each shard will serve one thread. If I create 100 shard then concurrently 100 threads can access the values. Now, suppose I have a Key =”A” and it’s stored in the Segment 1. Now if two threads are concurrently wants to access the values with the Key “A” then will the second thread will wait till the first thread leaves the lock of the Shard?
Do you know the logic to choose the Shard to store the key value?
Suppose a value with the Key “A” inserted in to #5 segment. Now two threads wants to read the value with the key “A”. Then what would happens will the second thread will wait for the first thread to complete it’s task.
Secondly, Could you explain that how Concurrent Hashmap choose segment to store the data or to retrieve.
To me, its some complex mathematics for locating the segment.
int hash = hash(key.hashCode());int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
To read value, it used UNSAFE.getObjectVolatile();
Refer: https://github.com/openjdk/jdk/blob/master/src/jdk.unsupported/share/classes/sun/misc/Unsafe.java
https://en.wikipedia.org/wiki/Volatile_variable
And
https://howtodoinjava.com/java-examples/usage-of-class-sun-misc-unsafe/
Nice article..Thanks for that..Suppose I am having a concurrent hashmap with Concurrency level as 2 and i need to read/write values in it by using multiple threads .In the scenario, if one thread has locked up one segment and checking the value and if another thread comes in will it bypass that segment or wait ?
In case of bypass, how come it will check the locked up segment..(because my scenario is to check whole map for a particular count).
You will have more clear picture after reading this post:
https://howtodoinjava.com/interview-questions/hashmap-concurrenthashmap-interview-questions/
“When getting data, a volatile read is used without any synchronization. If the volatile read results in a miss, then the lock for that segment is obtained and entry is again searched in synchronized block.”