What is Java ConcurrentHashMap?

ConcurrentHashMap is the thread-safe Java map implementation in java.util.concurrent. It allows any number of concurrent reads and a bounded number of concurrent writes without ever locking the whole map, which is what separates it from the older strategy of wrapping a HashMap in a global lock. It implements ConcurrentMap<K,V>, which strengthens the Map contract by requiring compound operations such as putIfAbsent, compute and merge to be carried out atomically.

Why Not Just Use HashMap?

A plain HashMap has no thread-safety guarantees at all, and the failure modes are worse than lost data. Two threads writing during a resize can corrupt the internal table; on Java 7 and earlier this could spin a reader into an infinite loop, pinning a CPU core indefinitely. Even without a resize, concurrent updates to the same bin silently drop writes.

Wrapping it with Collections.synchronizedMap() fixes correctness by serialising every operation through a single monitor, but that makes the map a bottleneck: readers block readers. ConcurrentHashMap exists to give you safety without paying that price, and it is the standard answer to a map-shaped race condition.

How ConcurrentHashMap Works (Java 8 and Later)

Most explanations of ConcurrentHashMap describe segments and a fixed number of striped locks. That design was removed in Java 8 and has been wrong for over a decade. The current implementation is a single array of bins, locked individually:

  • Inserting into an empty bin uses a compare-and-swap, with no lock at all.
  • Inserting into an occupied bin synchronizes on that bin's head node only. Contention is per-bin, so two writes collide only when their keys hash to the same bin.
  • Reads never lock. Bins and node values are volatile, so a get() is a plain memory read and readers never block writers or each other.
  • A bin that reaches eight entries is converted to a red-black tree, provided the table is at least 64 buckets — below that the table resizes instead. This caps worst-case lookup at O(log n) even under a hash collision attack.
  • size() is maintained as a set of striped counters rather than one contended field, which is why it is an estimate under concurrent modification. mappingCount() is the preferred accessor because it returns a long.

The concurrencyLevel constructor argument survives only for source compatibility. It is now a sizing hint and no longer controls the number of locks.

Behaviours Developers Get Wrong

Null keys and values are forbidden. This is deliberate rather than an oversight. In a single-threaded HashMap you can disambiguate a null return with containsKey(); under concurrency that second call may see a different map, so the ambiguity would be unresolvable. ConcurrentHashMap removes it by throwing NullPointerException instead.

Iterators are weakly consistent. They never throw ConcurrentModificationException, but they also give you no snapshot. An iterator reflects the map at some point at or after its creation and may or may not show concurrent updates. Do not iterate expecting a consistent view.

computeIfAbsent holds the bin lock while your function runs. If that function updates the same map, you can deadlock or get an IllegalStateException. It is documented and still widely unknown — keep mapping functions short, pure, and free of any reference back to the map.

Individual operations are atomic; sequences are not. This is the most common mistake in production code:

// BROKEN — another thread can insert between the two calls
if (!map.containsKey(key)) {
    map.put(key, value);
}

// Correct — one atomic operation
map.putIfAbsent(key, value);

// Correct — atomic read-modify-write
map.merge(key, 1, Integer::sum);

ConcurrentHashMap vs HashMap vs synchronizedMap

ConcurrentHashMapHashMapsynchronizedMapHashtable
Thread-safeYesNoYesYes
Lock granularityPer binWhole mapWhole map
Reads block?NeverYesYes
Null key or valueNeitherBothBothNeither
IteratorWeakly consistentFail-fastFail-fast, manual syncFail-fast
Use whenShared across threadsConfined to one threadLegacy codeDon't

For a map that is confined to one thread or never mutated after publication, HashMap is still faster and the right default. ConcurrentHashMap earns its place the moment the map is shared and written to — as an in-memory cache, a request counter, a session registry, or a memoization table.

The Limit: ConcurrentHashMap Is a Single-JVM Guarantee

Every guarantee above ends at the JVM boundary. Run two instances of your service and you have two independent maps that know nothing about each other. putIfAbsent no longer means "if absent" — it means "if absent on this node." Two nodes will both win the same race.

The consequences are quiet rather than loud. A local cache diverges between nodes and starts serving different answers to identical requests. Counters undercount by roughly the number of instances. "Only one worker picks up this job" stops holding the moment you scale past one replica — which is usually the moment you deploy to production.

A Distributed ConcurrentHashMap With Redisson

Redisson, the Valkey and Redis Java client, declares RMap<K,V> as extends ConcurrentMap<K,V>, RExpirable, RMapAsync<K,V>, RDestroyable. The interface you already program against is the interface it implements, so the migration is one line:

// Single JVM
ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();

// Shared by every JVM, backed by Valkey or Redis
ConcurrentMap<String, Integer> map = redisson.getMap("inventory");

map.putIfAbsent("widgets", 0);
map.replace("widgets", 0, 5);

putIfAbsent and replace(key, old, new) keep their atomicity, except it now holds across the whole cluster rather than one process. Redisson implements each as a single server-side script, so it costs one round trip and no lock.

compute, computeIfAbsent and merge are atomic too, but by a different mechanism worth understanding. Your function is Java, so it cannot run on the server; Redisson takes the per-key lock, runs the function client-side, and writes the result back. That is correct but costs several round trips, so prefer a native operation where one exists — for the common counter case, RMap adds addAndGet(key, delta), a single atomic increment on the server.

That per-key lock is also exposed directly, which ConcurrentHashMap cannot do: map.getLock(key), map.getReadWriteLock(key) and map.getSemaphore(key) bind a synchronizer to a single entry. See Java distributed lock, and Redis data structures in Java for how the map is stored as a Redis hash.

RMapCache adds per-entry TTL and max-idle eviction, which ConcurrentHashMap has no notion of; RLocalCachedMap keeps a near cache in JVM memory. Both are in the Apache 2.0 community edition. Partitioning one map across cluster masters with RClusteredMap is a Redisson PRO feature. See distributed caching.

Two honest caveats. RMap matches keys by their serialized state rather than hashCode() and equals(), so your codec must encode equal keys identically. And every operation is a network round trip — microseconds at best, against nanoseconds for a local map. That gap is real, and closing it is exactly what RLocalCachedMap is for; it is not a reason to use a distributed map where a local one is correct.

Java ConcurrentHashMap: Frequently Asked Questions

What Makes ConcurrentHashMap Thread-Safe?

It locks individual bins rather than the whole map. Writing to an empty bin uses a compare-and-swap with no lock; writing to an occupied one synchronizes on that bin's head node. Reads are volatile memory accesses and never lock, so readers never block writers or each other.

Why Does ConcurrentHashMap Not Allow Null Keys or Values?

Because a null return from get would be ambiguous. In a single-threaded HashMap you can resolve it by calling containsKey, but under concurrency that second call may observe a different map, so the ambiguity cannot be resolved. Forbidding null removes the problem rather than leaving it to the caller.

Is ConcurrentHashMap Faster Than HashMap?

No. For single-threaded access HashMap is faster, because ConcurrentHashMap pays for volatile reads and the machinery that supports concurrent writes. It wins only when a map is genuinely shared across threads, where the alternative is a global lock that serialises every operation.

Does ConcurrentHashMap Work Across Multiple JVMs?

No. Its guarantees stop at the process boundary, so every instance of your application holds a separate map. Sharing state across nodes needs a distributed implementation such as Redisson's RMap, which implements the same ConcurrentMap interface but stores entries in Valkey or Redis.

Similar terms