Java Tutorial
🔍

Java ConcurrentHashMap for Thread Safety

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.

// File: BeforeConcurrentHashMap.java // Illustrative only - concurrent structural modification of a plain HashMap // is undefined behavior. This can throw ConcurrentModificationException, // silently lose entries, or in rare cases even corrupt the map's internal // structure - the exact outcome is not deterministic. import java.util.*; import java.util.concurrent.CountDownLatch; public class BeforeConcurrentHashMap { static Map<Integer, String> scores = new HashMap<>(); public static void main(String[] args) throws InterruptedException { int threadCount = 8; CountDownLatch done = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int id = i; new Thread(() -> { for (int j = 0; j < 1000; j++) { scores.put(id * 1000 + j, "player-" + id); } done.countDown(); }).start(); } done.await(); System.out.println("Size: " + scores.size()); } }

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.

// File: AfterConcurrentHashMap.java import java.util.*; import java.util.concurrent.*; public class AfterConcurrentHashMap { static Map<Integer, String> scores = new ConcurrentHashMap<>(); public static void main(String[] args) throws InterruptedException { int threadCount = 8; CountDownLatch done = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int id = i; new Thread(() -> { for (int j = 0; j < 1000; j++) { scores.put(id * 1000 + j, "player-" + id); } done.countDown(); }).start(); } done.await(); System.out.println("Size: " + scores.size()); } }
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.

// File: ComputeMethodsExample.java import java.util.concurrent.*; public class ComputeMethodsExample { public static void main(String[] args) { ConcurrentHashMap<String, Integer> wordCounts = new ConcurrentHashMap<>(); String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"}; for (String word : words) { wordCounts.merge(word, 1, Integer::sum); } System.out.println("apple: " + wordCounts.get("apple")); System.out.println("banana: " + wordCounts.get("banana")); System.out.println("cherry: " + wordCounts.get("cherry")); } }
Output:
apple: 3
banana: 2
cherry: 1

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

// File: ComputeIfAbsentCacheExample.java import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class ComputeIfAbsentCacheExample { public static void main(String[] args) { ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>(); AtomicInteger computations = new AtomicInteger(); for (int i = 0; i < 3; i++) { int result = cache.computeIfAbsent("expensive-key", key -> { computations.incrementAndGet(); return 42; }); System.out.println("Result: " + result); } System.out.println("Times actually computed: " + computations.get()); } }
Output:
Result: 42
Result: 42
Result: 42
Times actually computed: 1

Basic Example

Two threads add points for the same player at the same time, using merge() to keep every update safe with no lock needed.

import java.util.concurrent.ConcurrentHashMap; public class Main { public static void main(String[] args) throws InterruptedException { ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>(); Runnable addPoints = () -> { for (int i = 0; i < 100; i++) { scores.merge("player", 1, Integer::sum); } }; Thread t1 = new Thread(addPoints); Thread t2 = new Thread(addPoints); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("player score: " + scores.get("player")); } }

Output

player score: 200

How it works:

  1. ›scores.merge("player", 1, Integer::sum) reads the current value for "player", adds 1 to it, and writes the result back as a single atomic per-key operation.
  2. ›Both threads call merge() 100 times each on the same key, and since each call is atomic, no update from either thread is ever silently lost.
  3. ›t1.join() and t2.join() wait for both threads to finish before the score is printed, so 200 is the only possible output every time this runs.

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.

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

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

// File: NullValueMistake.java import java.util.concurrent.ConcurrentHashMap; public class NullValueMistake { public static void main(String[] args) { ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); try { map.put("key", null); } catch (NullPointerException e) { System.out.println("Caught: " + e.getClass().getSimpleName()); } } }
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

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →