ConcurrentMap is an interface in java.util.concurrent for maps that many threads can read and update at the same time, with atomic methods such as putIfAbsent(), compute() and merge() that check and change an entry in one step. Its two JDK implementations are ConcurrentHashMap and ConcurrentSkipListMap.
We use a ConcurrentMap for shared state in multi-threaded code, such as request counters in a web app, a cache of loaded configuration or a registry of open connections, where a plain HashMap would lose updates.
The following example runs every atomic method once on a map of page views, with the result of each call as a comment.
ConcurrentMap<String, Integer> views = new ConcurrentHashMap<>();
views.put("home", 1);
Integer existing = views.putIfAbsent("home", 5); // 1, no change
Integer added = views.putIfAbsent("cart", 2); // null, key added
int home = views.merge("home", 1, Integer::sum); // 2
int cart = views.compute("cart", (k, v) -> v == null ? 1 : v * 10); // 20
boolean replaced = views.replace("cart", 20, 0); // true
boolean removed = views.remove("home", 99); // false, value is 2
int missing = views.getOrDefault("login", 0); // 0
Notice that replace() and remove() take the expected old value, so they change the entry only when no other thread has changed it first. We start with the bug that these methods prevent, go through each method, look at the implementations and their rules for null, iteration and size(), and finish with a counter used in real web apps.
1. Why Read-Modify-Write Fails With Many Threads
A HashMap is not thread-safe. Two threads that call put() at the same time can corrupt its buckets, and the map loses entries. Wrapping it with Collections.synchronizedMap() makes each single call safe, but a sequence of calls is still a race.
Code that reads a value, computes a new one and writes it back is called a read-modify-write sequence. Two threads can both read the old value 5 and both write 6, so one increment is lost even though every single call was thread-safe.

ConcurrentMap<String, Integer> racy = new ConcurrentHashMap<>();
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
for (int i = 0; i < 10_000; i++) {
pool.submit(() -> {
Integer v = racy.get("home"); // two threads can read the same value
racy.put("home", v == null ? 1 : v + 1);
});
}
}
int lost = racy.get("home"); // often less than 10000, differs per run
The fixed version replaces the two calls with one merge() call. The map runs the read and the write as one atomic operation for that key, and the ExecutorService closes and waits for all tasks at the end of the try-with-resources block, which Java 19 and later support.
ConcurrentMap<String, Integer> safe = new ConcurrentHashMap<>();
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
for (int i = 0; i < 10_000; i++) {
pool.submit(() -> safe.merge("home", 1, Integer::sum));
}
}
int counted = safe.get("home"); // 10000
Thread-safe methods do not make a sequence of calls thread-safe, so every read-modify-write on a shared map must go through one atomic method.
2. The Atomic Methods of ConcurrentMap
The ConcurrentMap interface was added in Java 5 with four atomic methods, and Java 8 added the functional ones. Each method in the table checks the current state of one key and updates it without a gap in which another thread could change the entry.
| Method | What it does atomically | Returns |
|---|---|---|
| putIfAbsent(k, v) | Adds the entry only when the key is missing | The existing value, or null when it added the entry |
| remove(k, v) | Removes the entry only when the key maps to v | true if removed |
| replace(k, v) | Replaces the value only when the key exists | The old value, or null |
| replace(k, old, new) | Replaces the value only when it equals old | true if replaced |
| computeIfAbsent(k, f) | Calls f and stores its result when the key is missing | The current or new value |
| computeIfPresent(k, f) | Recomputes the value when the key exists | The new value, or null when removed |
| compute(k, f) | Recomputes the value whether the key exists or not | The new value, or null when removed |
| merge(k, v, f) | Stores v, or combines it with the old value through f | The new value, or null when removed |
When a function passed to compute(), computeIfPresent() or merge() returns null, the map removes the entry. That rule gives us a clean way to decrement a counter and drop it at zero.
ConcurrentMap<String, Integer> seats = new ConcurrentHashMap<>(Map.of("show", 1));
Integer left = seats.computeIfPresent("show", (k, v) -> v > 1 ? v - 1 : null); // null, entry removed
boolean stillThere = seats.containsKey("show"); // false
3. computeIfAbsent() for Values That Are Expensive to Create
The method computeIfAbsent() is the standard way to build a cache entry on first use. In a ConcurrentHashMap, the whole call is atomic, and the function runs at most once per call when the key is absent and not at all when it is present.
For example, a shop service converts prices into other currencies and loads each exchange rate from a slow remote API. With computeIfAbsent(), a hundred parallel requests for the same currency trigger one remote call, and the other threads wait for its result.
static double loadRate(String currency) {
return currency.equals("EUR") ? 0.92 : 1.0; // stands in for a remote call
}
ConcurrentMap<String, Double> rates = new ConcurrentHashMap<>();
double eur = rates.computeIfAbsent("EUR", k -> loadRate(k)); // 0.92, loaded once
double again = rates.computeIfAbsent("EUR", k -> 99.0); // 0.92, function not called
Two rules keep this safe. The function must be short, because other updates to the same part of the table wait while it runs, and it must not modify the same map, otherwise ConcurrentHashMap can throw an IllegalStateException with the message “Recursive update”.
The once-only guarantee belongs to ConcurrentHashMap, not to the interface. The default methods in ConcurrentMap and the overrides in ConcurrentSkipListMap may call the function more than once under contention and keep one result, so the function must not have side effects such as sending an email.
4. Null Keys and Values Are Rejected
Both JDK implementations throw a NullPointerException for a null key or value. The reason is that get() returning null must mean “no entry”. In a single-threaded HashMap we can call containsKey() to tell a missing key from a null value, but another thread can change the map between the two calls.
ConcurrentMap<String, Integer> stock = new ConcurrentHashMap<>();
stock.put("apple", null); // NullPointerException
When a value can be missing, we store a placeholder such as Optional.empty() or a constant, or we leave the key out and read it with getOrDefault().
ConcurrentMap<String, Optional<Integer>> prices = new ConcurrentHashMap<>();
prices.put("apple", Optional.empty()); // stored, means "no price yet"
int price = prices.get("apple").orElse(0); // 0
5. ConcurrentMap Implementations
The JDK has two classes that implement the interface. We pick by whether the keys must stay sorted.
5.1. ConcurrentHashMap
A ConcurrentHashMap is a hash table built for concurrency. Reads take no lock at all. Since Java 8, a write to an empty bucket uses a compare-and-swap instruction, and a write to a filled bucket locks only the first node of that bucket, so threads that update different keys rarely wait for each other.
Before Java 8, the map was split into 16 segments with one lock each, and many older articles still describe that design. The concurrencyLevel constructor argument survives only as a sizing hint for the initial table.
ConcurrentMap<String, Integer> plain = new ConcurrentHashMap<>(); // 16 bins, load factor 0.75
ConcurrentMap<String, Integer> sized = new ConcurrentHashMap<>(1_000); // sized for 1000 mappings
ConcurrentMap<String, Integer> copied = new ConcurrentHashMap<>(Map.of("a", 1)); // copies a map
Set<String> users = ConcurrentHashMap.newKeySet(); // a concurrent Set
boolean addedUser = users.add("lokesh"); // true
5.2. ConcurrentSkipListMap
A ConcurrentSkipListMap keeps its keys sorted, like a TreeMap, and implements ConcurrentNavigableMap. It costs O(log n) per operation instead of the average O(1) of a hash table, so we choose it only when we need ordered keys, ranges or the smallest and largest key.
ConcurrentNavigableMap<Integer, String> scores = new ConcurrentSkipListMap<>();
scores.put(70, "bob");
scores.put(95, "lokesh");
scores.put(82, "alex");
String best = scores.lastEntry().getValue(); // "lokesh"
SortedMap<Integer, String> passed = scores.tailMap(80); // {82=alex, 95=lokesh}
5.3. Comparing the Thread-Safe Maps
Older code often uses Hashtable or a synchronized wrapper. Both lock the whole map on every call, so only one thread at a time can read or write.
| Map | Locking | Sorted | null keys or values | Iterator |
|---|---|---|---|---|
| ConcurrentHashMap | Per bucket on writes, none on reads | No | Rejected | Weakly consistent |
| ConcurrentSkipListMap | Lock-free | Yes | Rejected | Weakly consistent |
| Collections.synchronizedMap() | Whole map on every call | Depends on the wrapped map | As the wrapped map | Fail-fast, needs manual locking |
| Hashtable | Whole map on every call | No | Rejected | Fail-fast |
The synchronizedMap vs ConcurrentHashMap article compares the two most common choices in more depth. In new code, Hashtable has no advantage over ConcurrentHashMap.
6. Iteration, size() and Bulk Operations
Iterators of both implementations are weakly consistent. They never throw ConcurrentModificationException, they visit each entry that existed when the iterator was created once, and they may or may not show changes made after the iterator was created. We can also modify the map while we loop over it.
ConcurrentMap<String, Integer> carts = new ConcurrentHashMap<>(Map.of("a", 1, "b", 0, "c", 3));
for (String user : carts.keySet()) {
carts.remove("b"); // no ConcurrentModificationException
}
int total = carts.values().stream().mapToInt(Integer::intValue).sum(); // 4
long nonEmpty = carts.entrySet().stream().filter(e -> e.getValue() > 0).count(); // 2
While other threads write, size(), isEmpty() and containsValue() return a value that may already be out of date. They are fine for monitoring and logs, but not for decisions such as “stop accepting orders when the map has 100 entries”. A ConcurrentHashMap also offers mappingCount(), which returns a long for maps with more than Integer.MAX_VALUE entries.
The class ConcurrentHashMap also has bulk methods such as forEach(), search() and reduceValues() that take a parallelism threshold. When the map has more entries than the threshold, the work runs in parallel on the common ForkJoinPool.
ConcurrentHashMap<String, Integer> stockLevels = new ConcurrentHashMap<>(Map.of("apple", 5, "pear", 0, "plum", 7));
long count = stockLevels.mappingCount(); // 3
Integer sum = stockLevels.reduceValues(1_000, Integer::sum); // 12
String outOfStock = stockLevels.search(1_000, (k, v) -> v == 0 ? k : null); // "pear"
7. Counting Requests per Endpoint in a Web App
A REST service wants to show how many requests each endpoint got since startup, and hundreds of request threads update the numbers at the same time. A ConcurrentHashMap with LongAdder values handles this with one line per request. The map creates each counter once, and a LongAdder spreads the increments over several cells, so threads that hit the same endpoint do not wait for each other.
static void track(String endpoint) {
HITS.computeIfAbsent(endpoint, k -> new LongAdder()).increment();
}
static final ConcurrentMap<String, LongAdder> HITS = new ConcurrentHashMap<>();
track("/orders");
track("/orders");
track("/login");
long orders = HITS.get("/orders").sum(); // 2
long login = HITS.get("/login").sum(); // 1
When only a few threads update the same key, merge(key, 1L, Long::sum) works as well. A LongAdder pays off when many threads hit the same key, because merge() makes them update the same bucket one after another.
8. ConcurrentMap FAQs
Teams ask these questions when they move shared state from a HashMap to a concurrent map.
8.1. What is the difference between ConcurrentMap and ConcurrentHashMap?
The type ConcurrentMap is the interface, and ConcurrentHashMap is its hash-based implementation. We declare fields as ConcurrentMap when only the atomic methods matter, and as ConcurrentHashMap when we use its extra methods such as mappingCount(), newKeySet() or the parallel bulk operations.
8.2. Is ConcurrentHashMap completely thread-safe?
Each method call is thread-safe and atomic for its key. A sequence of calls, such as containsKey() followed by put(), is not, as section 1 shows. The values must also be thread-safe if threads change them after reading them from the map.
8.3. Does ConcurrentHashMap still use segments?
No. Java 8 replaced the 16 segment locks with compare-and-swap writes and a lock on the first node of each bucket. The class keeps a Segment type only for serialization compatibility with older versions.
8.4. How do we convert a HashMap into a ConcurrentMap?
We pass it to the constructor, as in new ConcurrentHashMap<>(hashMap). The copy throws a NullPointerException when the source map contains a null key or value, so we filter those out first.
8.5. Why does size() give different results while threads are running?
The method returns an estimate that reflects the map at some moment during the call. Other threads keep adding and removing entries, so the number can be out of date by the time we use it. When the count matters, we keep a separate atomic counter and update it in the same code path.
9. Conclusion
A ConcurrentMap gives shared maps a set of atomic methods, so a check and an update on one key happen as a single step. With merge(), compute() and computeIfAbsent(), the classic lost-update bug of get() followed by put() disappears.
For most code, ConcurrentHashMap is the right implementation, and ConcurrentSkipListMap is the choice when the keys must stay sorted. Both reject null, both iterate without ConcurrentModificationException, and both report size() as an estimate under load. The Java Collections guide places them among the other map types, and the Java concurrency tutorial covers the threads that use them.
10. References
- ConcurrentMap Javadoc (Java 25)
- ConcurrentHashMap Javadoc (Java 25)
- ConcurrentSkipListMap Javadoc (Java 25)
- java.util.concurrent package summary
Happy Learning !!