Question 18 Β· Section 4

What is ConcurrentHashMap?

Unlike Collections.synchronizedMap(new HashMap<>()) (a single lock for the entire map), CHM allows many threads to work in parallel by locking only individual buckets.

Language versions: English Russian Ukrainian

🟒 Junior Level

ConcurrentHashMap is a thread-safe Map for multi-threaded applications.

Unlike Collections.synchronizedMap(new HashMap<>()) (a single lock for the entire map), CHM allows many threads to work in parallel by locking only individual buckets.

Key difference from HashMap:

  • HashMap: not thread-safe
  • ConcurrentHashMap: thread-safe, no locks for reads
Map<String, Integer> map = new ConcurrentHashMap<>();

// Safe from multiple threads
map.put("A", 1);
map.get("A");  // β†’ 1

// null is not allowed!
map.put("A", null);  // β†’ NullPointerException!

Rule of thumb:

  • Single thread or thread-local β†’ HashMap
  • Multiple threads reading and writing β†’ ConcurrentHashMap
  • Multiple threads only reading β†’ HashMap is fine (after safe publication)

🟑 Middle Level

Evolution

Java 7: Segment Locking

Java 7 β€” historical reference, not used in modern code.

Map divided into 16 segments
  β†’ Threads on different segments don't block each other
  β†’ size() = locks ALL segments!

Java 8+: Node-level Locking

CAS for empty buckets (no locks!)
Synchronized only for collisions (per bucket!)
  β†’ Maximum parallelism

Atomic operations

map.putIfAbsent(key, value);  // Insert if absent

map.computeIfAbsent(key, k -> new ArrayList<>())
   .add(value);  // Compute and insert

map.merge(key, 1, Integer::sum);  // Atomic update

Features

// ❌ null is not allowed!
map.put(key, null);  // β†’ NullPointerException

// ⚠️ size() is approximate!
int size = map.size();  // May change immediately!

// βœ… mappingCount() for large collections
long count = map.mappingCount();  // > 2^31

// βœ… Iterators do NOT throw Exception
// β†’ Weakly consistent
// β†’ Weakly consistent = iterator sees data at creation time and may (but is not required to) see subsequent changes. Never throws ConcurrentModificationException.
for (var entry : map) { ... }  // Safe

πŸ”΄ Senior Level

Lock-free Reads

// get() WITHOUT locks!
// Through volatile fields:
//   table array β†’ volatile
//   Node.val β†’ volatile
//   Node.next β†’ volatile

// Happens-Before guarantee:
// β†’ Reader sees the latest write

CAS + Synchronized

CAS (Compare-And-Swap) β€” an atomic CPU instruction: β€œwrite the new value only if the current value equals the expected one.” Works without locks β€” if two threads write simultaneously, one succeeds, the other retries.

put(key, value):
  1. Bucket is empty β†’ CAS (no locks!)
  2. Bucket is occupied β†’ synchronized on HEAD
     β†’ Only this bucket is locked
     β†’ Others work in parallel

Parallel Resizing

On resize:
  β†’ ForwardingNode in old buckets
  β†’ Writer threads SEE ForwardingNode
  β†’ Call helpTransfer() β†’ HELP with resizing!

β†’ Distributed resize, no full lock

CounterCells

Instead of a single volatile size:
  β†’ CounterCell array (like LongAdder)
  β†’ Each thread increments ITS OWN cell
  β†’ size() = sum of all cells

β†’ Minimal contention!

Compute pitfall

// ❌ Long-running operation in compute
map.computeIfAbsent(key, k -> {
    Thread.sleep(10000);  // Blocks the bucket for 10 seconds!
    return value;
});

// ❌ Recursive access β†’ Deadlock!
map.computeIfAbsent(key1, k -> {
    return map.get(key2);  // May wait on the same bucket!
});

Java 21: Virtual Threads

synchronized in CHM may "pin" virtual threads
  β†’ But critical sections are VERY short
  β†’ Rarely a problem in practice

If it is a problem:
  β†’ async-profiler will show blocked time

Production Experience

Real scenario: Bad hashCode

// All keys β†’ one bucket
// β†’ Synchronized on a single bucket
// β†’ 100 threads waiting β†’ 0 parallelism!

// Solution: rewrite hashCode()
// Result: +500% throughput

Best Practices

  1. Default choice for concurrency
  2. null is not allowed β€” protects against ambiguity
  3. computeIfAbsent β€” atomic initialization
  4. No long-running operations in compute
  5. mappingCount() > size() for > 2^31 elements
  6. Bad hashCode β†’ the main enemy of parallelism
  7. Weakly consistent iterators

Summary for Senior

  • CAS for empty buckets, synchronized for collisions
  • Lock-free reads through volatile
  • Parallel resizing with helpTransfer()
  • CounterCells = scalable counter
  • compute β†’ no long operations, no recursion
  • null not allowed β€” a philosophical decision
  • Bad hashCode β†’ contention on a single bucket

🎯 Interview Cheat Sheet

Must know:

  • ConcurrentHashMap β€” thread-safe Map, unlike HashMap it allows parallel work without a full map lock
  • Java 8+: CAS for empty buckets (lock-free) + synchronized only on the bucket for collisions (instead of Segment locking from Java 7)
  • Lock-free reads through volatile: table, Node.val, Node.next β€” get() without locks
  • null not allowed (keys and values) β€” protects against ambiguity (null = key absent vs null = value)
  • Iterators are weakly consistent β€” do not throw ConcurrentModificationException, may not see latest changes
  • Parallel resizing: ForwardingNode + helpTransfer() β€” writer threads help with resizing
  • CounterCells (like LongAdder) β€” each thread increments its own cell, size() = sum
  • size() is approximate, mappingCount() for > 2^31 elements

Frequent follow-up questions:

  • How does CHM differ from Collections.synchronizedMap? β€” synchronizedMap locks the ENTIRE map on every operation. CHM locks only one bucket (synchronized) or doesn’t lock at all (CAS).
  • Why are null values not allowed in CHM? β€” To distinguish β€œkey not present” (get = null) from β€œvalue is null”. In HashMap this is impossible, creating ambiguity.
  • What happens with a long-running operation in computeIfAbsent? β€” The bucket is locked for the entire computation. Other threads writing to the same bucket will wait. Recursive access β†’ deadlock.
  • How does CHM resize without a full lock? β€” It places ForwardingNode in old buckets. Writer threads, seeing it, call helpTransfer() and help transfer data.

Red flags (DO NOT say):

  • ❌ β€œCHM uses Segment locking” β€” that’s Java 7, Java 8+ uses node-level locking
  • ❌ β€œCHM fully locks the map on write” β€” it locks only one bucket, others keep working
  • ❌ β€œCHM supports null values” β€” no, NullPointerException for null keys and values
  • ❌ β€œsize() in CHM is exact” β€” it’s approximate, may change immediately after the call

Related topics:

  • [[19. How does ConcurrentHashMap ensure thread-safety]]
  • [[14. What is Map and what implementations exist]]
  • [[20. What is CopyOnWriteArrayList]]
  • [[15. Difference between HashMap, LinkedHashMap, and TreeMap]]