Java Tutorial
🔍

ConcurrentHashMap

ConcurrentHashMap

A plain HashMap is not safe for concurrent access — its internal bucket and resize logic assumes only one thread ever touches it at a time. ConcurrentHashMap, part of java.util.concurrent since Java 5, is safe for concurrent access from many threads at once, and goes further than simple thread safety by offering atomic per-key operations like compute(), computeIfAbsent(), and merge().

What Is ConcurrentHashMap?

ConcurrentHashMap<K, V> is a Map implementation designed from the ground up for concurrent access, coordinating at the level of individual buckets rather than the whole map. It also adds a set of atomic per-key operations — merge(), compute(), computeIfAbsent() — that a coarsely-locked map still cannot provide safely without extra, manual synchronization.

Why Thread Safety Matters Here

Concurrent structural modification of a plain HashMap is undefined behavior — the exact outcome varies and is never something to rely on. A resize triggered by two threads at nearly the same moment can corrupt the map's internal linked structure, lose entries silently, or throw ConcurrentModificationException mid-iteration, and none of these failures are guaranteed to show up in a quick local test the way they will under real concurrent traffic.

How It Works

One sentence before the diagram: HashMap offers no coordination at all between threads, while ConcurrentHashMap coordinates only at the bucket level each specific key falls into.

HashMap under concurrent writes:          ConcurrentHashMap under concurrent writes:

Thread A -- put() --\                     Thread A -- put(keyA) --> bucket 3 (locked briefly)
Thread B -- put() ---+ same internal      Thread B -- put(keyB) --> bucket 7 (locked briefly)
Thread C -- put() --/  structure,         Thread C -- put(keyC) --> bucket 3 (waits for A)
            no coordination at all

Result: undefined - lost entries,         Result: every entry safely stored;
ConcurrentModificationException,          contention only occurs when two
or a corrupted internal structure         threads hash to the same bucket

ConcurrentHashMap rejects null keys and null values outright, on purpose — a null returned from get() cannot be reliably distinguished from "key not present" once concurrent modification is possible.

Code Examples

Concurrent structural modification of a plain HashMap is undefined behavior — the exact outcome varies and is never something to rely on.

1// File: BeforeConcurrentHashMap.java 2// Illustrative only - concurrent structural modification of a plain HashMap 3// is undefined behavior. This can throw ConcurrentModificationException, 4// silently lose entries, or in rare cases even corrupt the map's internal 5// structure - the exact outcome is not deterministic. 6import java.util.*; 7import java.util.concurrent.CountDownLatch; 8 9public class BeforeConcurrentHashMap { 10 11 static Map<Integer, String> scores = new HashMap<>(); 12 13 public static void main(String[] args) throws InterruptedException { 14 int threadCount = 8; 15 CountDownLatch done = new CountDownLatch(threadCount); 16 17 for (int i = 0; i < threadCount; i++) { 18 final int id = i; 19 new Thread(() -> { 20 for (int j = 0; j < 1000; j++) { 21 scores.put(id * 1000 + j, "player-" + id); 22 } 23 done.countDown(); 24 }).start(); 25 } 26 27 done.await(); 28 System.out.println("Size: " + scores.size()); 29 } 30}

Swapping in ConcurrentHashMap makes the identical code fully safe, with every entry guaranteed to be present regardless of how the eight threads happen to interleave.

1// File: AfterConcurrentHashMap.java 2import java.util.*; 3import java.util.concurrent.*; 4 5public class AfterConcurrentHashMap { 6 7 static Map<Integer, String> scores = new ConcurrentHashMap<>(); 8 9 public static void main(String[] args) throws InterruptedException { 10 int threadCount = 8; 11 CountDownLatch done = new CountDownLatch(threadCount); 12 13 for (int i = 0; i < threadCount; i++) { 14 final int id = i; 15 new Thread(() -> { 16 for (int j = 0; j < 1000; j++) { 17 scores.put(id * 1000 + j, "player-" + id); 18 } 19 done.countDown(); 20 }).start(); 21 } 22 23 done.await(); 24 System.out.println("Size: " + scores.size()); 25 } 26}
Output:
Size: 8000

Each of the eight threads writes into its own disjoint range of keys, so all 8000 entries are genuinely distinct — ConcurrentHashMap guarantees every one of them safely makes it into the map. merge(), compute(), and computeIfAbsent() go a step further, performing a read-modify-write sequence on a single key as one atomic operation — something a separate get() followed by put() cannot guarantee, even on a ConcurrentHashMap.

1// File: ComputeMethodsExample.java 2import java.util.concurrent.*; 3 4public class ComputeMethodsExample { 5 public static void main(String[] args) { 6 ConcurrentHashMap<String, Integer> wordCounts = new ConcurrentHashMap<>(); 7 String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"}; 8 9 for (String word : words) { 10 wordCounts.merge(word, 1, Integer::sum); 11 } 12 13 System.out.println("apple: " + wordCounts.get("apple")); 14 System.out.println("banana: " + wordCounts.get("banana")); 15 System.out.println("cherry: " + wordCounts.get("cherry")); 16 } 17}
Output:
apple: 3
banana: 2
cherry: 1

computeIfAbsent() guarantees its computation runs at most once per key, even under concurrent access from multiple threads.

1// File: ComputeIfAbsentCacheExample.java 2import java.util.concurrent.*; 3import java.util.concurrent.atomic.AtomicInteger; 4 5public class ComputeIfAbsentCacheExample { 6 public static void main(String[] args) { 7 ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>(); 8 AtomicInteger computations = new AtomicInteger(); 9 10 for (int i = 0; i < 3; i++) { 11 int result = cache.computeIfAbsent("expensive-key", key -> { 12 computations.incrementAndGet(); 13 return 42; 14 }); 15 System.out.println("Result: " + result); 16 } 17 18 System.out.println("Times actually computed: " + computations.get()); 19 } 20}
Output:
Result: 42
Result: 42
Result: 42
Times actually computed: 1

Real-World Example

A quiz app's live leaderboard accumulates points as players submit answers concurrently, using merge() to guarantee every point is counted regardless of how many submissions arrive at the same instant — the same pattern extends naturally to thread-safe caching via computeIfAbsent() and to safe iteration without ConcurrentModificationException, since ConcurrentHashMap's iterators are weakly consistent rather than fail-fast.

1// File: QuizLeaderboard.java 2import java.util.concurrent.ConcurrentHashMap; 3 4public class QuizLeaderboard { 5 private final ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>(); 6 7 public void recordAnswer(String player, int pointsEarned) { 8 scores.merge(player, pointsEarned, Integer::sum); 9 } 10 11 public int getScore(String player) { 12 return scores.getOrDefault(player, 0); 13 } 14}
1// File: QuizLeaderboardDemo.java 2import java.util.concurrent.CountDownLatch; 3 4public class QuizLeaderboardDemo { 5 public static void main(String[] args) throws InterruptedException { 6 QuizLeaderboard leaderboard = new QuizLeaderboard(); 7 int submissionThreads = 10; 8 CountDownLatch done = new CountDownLatch(submissionThreads); 9 10 for (int i = 0; i < submissionThreads; i++) { 11 new Thread(() -> { 12 leaderboard.recordAnswer("Player-A", 5); 13 done.countDown(); 14 }).start(); 15 } 16 17 done.await(); 18 19 System.out.println("Player-A score: " + leaderboard.getScore("Player-A")); 20 } 21}
Output:
Player-A score: 50

Ten threads each add 5 points for the same player at essentially the same moment, and the total comes out to exactly 50 every time. A mistake that appears often in fresher pull requests is updating a shared score with a separate get() and put() call — reading the current score, adding points, then writing it back — which is exactly the same check-then-act race this series' Race Conditions article covers, just against a map entry instead of a plain variable. merge(), used throughout QuizLeaderboard here, performs the read-add-write sequence as a single atomic per-key operation, so ten concurrent submissions for the same player are guaranteed to add up to exactly the right total.

Best Practices

Reach for ConcurrentHashMap by default whenever a map is shared across threads, rather than Collections.synchronizedMap() wrapping a HashMap, for its finer-grained concurrency and better throughput under contention.

Use merge(), compute(), or computeIfAbsent() for any read-modify-write sequence on a map entry, instead of a separate get() and put(), exactly as this article's leaderboard example does.

Do not treat size() as a perfectly consistent snapshot under concurrent modification — it is a close estimate while updates are in flight, and only exact once modifications have stopped.

Remember that ConcurrentHashMap rejects null keys and null values outright — use a sentinel value or Optional instead of relying on null to represent "absent" the way some code does with HashMap. merge() and compute() accept a BiFunction, and Integer::sum, used throughout this article, is a method reference implementing exactly that shape — the same atomicity guarantee merge() provides for a single key mirrors what compareAndSet() provides for a single atomic variable.

Common Mistakes

Using a separate get() and put() for an update is a check-then-act race even on a ConcurrentHashMap — being individually thread-safe does not make a two-step sequence atomic as a whole.

1// File: GetThenPutMistake.java 2// Illustrative only - get() then put() is a check-then-act race even on a 3// ConcurrentHashMap. Being individually thread-safe does not make this 4// two-step sequence atomic as a whole; concurrent updates can still be lost. 5import java.util.concurrent.ConcurrentHashMap; 6 7public class GetThenPutMistake { 8 static final ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>(); 9 10 static void addPoints(String player, int points) { 11 int current = scores.getOrDefault(player, 0); 12 scores.put(player, current + points); 13 } 14}

Assuming ConcurrentHashMap allows null keys or values the way HashMap does throws a NullPointerException immediately.

1// File: NullValueMistake.java 2import java.util.concurrent.ConcurrentHashMap; 3 4public class NullValueMistake { 5 public static void main(String[] args) { 6 ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); 7 8 try { 9 map.put("key", null); 10 } catch (NullPointerException e) { 11 System.out.println("Caught: " + e.getClass().getSimpleName()); 12 } 13 } 14}
Output:
Caught: NullPointerException

HashMap permits one null key and any number of null values; ConcurrentHashMap rejects both entirely, since a null return from get() cannot be reliably distinguished from "key not present" under concurrent modification.

Interview Questions

Q1. Why is a plain HashMap not safe for concurrent access?

Its internal bucket and resize logic assumes single-threaded access — concurrent structural modification, such as two threads triggering a resize at the same time, can corrupt internal data structures, lose entries, or throw ConcurrentModificationException, with the exact outcome undefined. Interviewers listen for whether you know this is genuinely undefined, not just "sometimes slow."

Q2. What is the difference between Collections.synchronizedMap() and ConcurrentHashMap in terms of concurrency granularity?

Collections.synchronizedMap() wraps a map with a single lock guarding every operation, serializing all access regardless of which keys are involved. ConcurrentHashMap coordinates at a much finer grain, so operations on different keys can generally proceed with little to no contention between them. This granularity distinction is the core of what interviewers are actually testing here.

Q3. Does ConcurrentHashMap allow null keys or null values?

No, both are rejected with a NullPointerException, exactly as demonstrated in this article's NullValueMistake example — this is a deliberate design choice, unlike HashMap, which permits both. The nuance worth stating is why: a null return would be ambiguous under concurrent modification.

Q4. What guarantee does merge() provide that a separate get() and put() does not?

merge() performs its read-combine-write sequence as a single atomic operation per key — a separate get() followed by put() is two independent steps, leaving a window where another thread's concurrent update can be silently overwritten, exactly the trap this article's GetThenPutMistake demonstrates.

Q5. Does iterating a ConcurrentHashMap throw ConcurrentModificationException if the map is modified during iteration?

No. ConcurrentHashMap's iterators are weakly consistent — they reflect the map's state at some point during the iteration without throwing, in contrast to HashMap's fail-fast iterators, which throw ConcurrentModificationException when they detect a structural change.

Q6. What is the difference between computeIfAbsent() and a plain "if absent, put" pattern?

computeIfAbsent() guarantees its computation function runs at most once per key even under concurrent access, exactly as demonstrated in this article's ComputeIfAbsentCacheExample. A manual "check then put" pattern is a check-then-act race that can run the computation more than once, or lose an update, under concurrent access.

Q7. Which Java version introduced ConcurrentHashMap?

Java 5, in 2004, as part of the same java.util.concurrent package introduced alongside ExecutorService, Future, and the other concurrency utilities covered throughout this section.

FAQs

Is ConcurrentHashMap slower than a plain HashMap?

For single-threaded use, yes, modestly — the extra coordination overhead has some cost even with no actual contention. For concurrent use, it is far faster than a coarse-grained alternative like Collections.synchronizedMap(), since contention is limited to the specific keys involved rather than the entire map.

Does ConcurrentHashMap guarantee a consistent snapshot when calling size()?

Not under concurrent modification — the returned value is a close estimate while updates are actively in flight, and only exact once no further modifications are happening.

Can ConcurrentHashMap be used as a drop-in replacement for HashMap?

In most cases, yes, since it implements the same Map interface — the two exceptions worth knowing are that ConcurrentHashMap rejects null keys and values, and its iteration order and exact behavior around concurrent modification differ.

What does "weakly consistent" iteration mean?

It means an iterator reflects the map's state at some point during the iteration, without guaranteeing it captures every single change made concurrently, but also without ever throwing an exception or corrupting the map as a result of those changes.

Is ConcurrentHashMap suitable for a cache?

Yes, it is a very common choice for an in-memory cache, especially combined with computeIfAbsent() to guarantee a given cache entry is computed at most once even under concurrent requests for the same key.

Does ConcurrentHashMap use a single lock internally like Collections.synchronizedMap()?

No. Modern ConcurrentHashMap implementations use fine-grained, per-bucket coordination — largely compare-and-swap based, with locking limited to specific situations — rather than one lock guarding the entire map, which is exactly what gives it much better throughput under concurrent access.

Can two threads call compute() on the same key at the same time safely?

Yes, safely — ConcurrentHashMap guarantees per-key atomicity for compute() and its relatives, so concurrent calls on the same key are safely serialized with no lost updates, while calls on different keys can proceed independently with little to no contention.

Summary

ConcurrentHashMap solves two related problems HashMap cannot handle safely under concurrency: basic thread safety for structural operations like put(), and atomic per-key read-modify-write operations through merge(), compute(), and computeIfAbsent() — something even a coarsely-locked map cannot provide without extra, manual synchronization.

The habit worth carrying forward from this article's leaderboard example is reaching for merge() or compute() the instant an update depends on a key's current value, rather than a separate get() and put() that reintroduces exactly the check-then-act race this series' Race Conditions article warns about.

What to Read Next