Java

ConcurrentHashMap Explained

A practical explanation of ConcurrentHashMap in Java, including thread safety, atomic methods, iteration behavior, and common mistakes.

Quick answer

ConcurrentHashMap is a thread-safe map implementation for Java programs where multiple threads need to read and update shared key-value data. It is usually a better choice than Hashtable or Collections.synchronizedMap(...) when the map is accessed heavily by many threads.

Use it for shared lookup tables, lightweight caches, counters, request coordination, and in-memory indexes. Do not treat it as a complete concurrency design by itself. The map can protect map operations, but it does not automatically make mutable values inside the map safe.

What ConcurrentHashMap is

ConcurrentHashMap implements the ConcurrentMap interface. It allows concurrent reads and updates without synchronizing the entire map object for every operation.

That matters because backend services often have shared state that many threads touch at the same time: cached user permissions, per-key counters, request deduplication state, or background job metadata. A coarse lock around the entire map can become a bottleneck. ConcurrentHashMap is designed to reduce that bottleneck for common map operations.

Why HashMap is not enough

HashMap is not safe for concurrent mutation. If one thread reads while another thread writes, or if two threads write at the same time without coordination, the program has a data race. Results can be stale, inconsistent, or wrong.

A common attempted fix is this:

Map<String, Integer> counts = Collections.synchronizedMap(new HashMap<>());

That can be correct for simple cases, but it synchronizes access through a shared wrapper. Under high read/write traffic, that single synchronization point can limit throughput.

ConcurrentHashMap exists for cases where many threads need safe access with better concurrency characteristics.

Atomic methods to prefer

The most important rule is simple: when an update depends on the current value, use one atomic map method instead of a sequence of separate calls.

MethodUse case
putIfAbsent(key, value)Store a value only if the key is missing.
computeIfAbsent(key, mappingFunction)Lazily create and store a value.
compute(key, remappingFunction)Recalculate a value from the current value.
merge(key, value, remappingFunction)Add or combine values, such as counters.
remove(key, value)Remove only if the key still maps to that value.

Avoid this pattern:

if (!map.containsKey(key)) {
    map.put(key, value);
}

Another thread can update the map between containsKey and put. Use putIfAbsent or computeIfAbsent instead.

Example: counting events

For simple counters, merge is compact and avoids a check-then-act race:

ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();

counts.merge("login", 1, Integer::sum);
counts.merge("login", 1, Integer::sum);

System.out.println(counts.get("login")); // 2

For high-volume counters, a mutable counter value such as LongAdder can reduce contention:

ConcurrentHashMap<String, LongAdder> counters = new ConcurrentHashMap<>();

counters.computeIfAbsent("login", key -> new LongAdder()).increment();
counters.computeIfAbsent("checkout", key -> new LongAdder()).increment();

The map operation is thread-safe, and LongAdder is designed for concurrent increments. This is a good example of choosing thread-safe values as well as a thread-safe map.

Example: lightweight cache

computeIfAbsent is useful for lazy loading:

ConcurrentHashMap<String, UserProfile> cache = new ConcurrentHashMap<>();

UserProfile profile = cache.computeIfAbsent(userId, id -> userRepository.loadProfile(id));

Keep the mapping function reasonably fast and predictable. If loading can block for a long time, fail, or require complex lifecycle management, a real cache library may be a better fit than a plain map.

Iteration behavior

Iterators from ConcurrentHashMap are weakly consistent. They do not throw ConcurrentModificationException when the map changes while you iterate, but they also do not promise a perfect snapshot of the map at one point in time.

That behavior is useful for monitoring, statistics, and best-effort scans. It is not enough for workflows that require a precise transactional snapshot.

Null keys and values

ConcurrentHashMap does not allow null keys or null values. This is intentional. In concurrent code, null can make it ambiguous whether a key is absent or whether a value exists but is null.

Use explicit values, optional wrappers, or a separate state object if you need to represent missing data.

When to use ConcurrentHashMap

Use ConcurrentHashMap when:

  • Multiple threads read and update the same map.
  • You need atomic per-key operations.
  • A synchronized map would be too coarse for the workload.
  • The data can be modeled as independent key-value entries.
  • Weakly consistent iteration is acceptable.

Consider another design when:

  • You need transactional updates across multiple keys.
  • Values inside the map are mutable and not thread-safe.
  • The data needs eviction, TTL, size limits, or refresh behavior.
  • A database, queue, actor, or cache library is the real owner of the state.

Common mistakes

The first mistake is assuming that separate calls become atomic because the map is concurrent. They do not. get, then modify, then put is still a sequence. Use compute, merge, or another atomic method.

The second mistake is storing mutable objects and assuming the map protects their internals. If the value is a List, HashSet, or custom object, that object may still need synchronization, immutability, or a concurrent implementation.

The third mistake is using ConcurrentHashMap to hide unclear ownership. If several parts of the system can mutate shared state at any time, the design may still be fragile even if the data structure is thread-safe.

Read HashMap vs Hashtable in Java for older synchronized map tradeoffs, HashMap Load Factor Explained for hash table behavior, and ArrayList vs LinkedList in Java for another Java collections comparison. You can also browse the topic clusters for the Java Collections content path.