Definition
- Java 7: array of 16
Segmentobjects, each a mini-HashMap with its ownReentrantLock; maximum concurrency = 16 - Java 8+: flat
Node[]table (no segments); reads use volatile (no lock); writes use CAS for empty buckets,synchronizedon the first Node for non-empty buckets - Treeification: same as HashMap – threshold 8, buckets convert to Red-Black trees
- Distributed resize: multiple threads help migrate buckets; each claims a stride via CAS on
transferIndex - Atomic operations: computeIfAbsent, merge, compute are all atomic per-key
Java 7: a bank with 16 sections, each with its own padlock. Two people in section 1 must wait, but sections 2-16 are free. Java 8: a bank where each individual drawer has its own lock. Two people at different drawers both proceed. Reading a drawer requires no key at all – just peek through the glass (volatile read). Expanding the cabinet: all employees help simultaneously, each taking a chunk of drawers to move.
Understanding the Concepts
How did Java 7 ConcurrentHashMap work?
Java 7's ConcurrentHashMap was an array of 16 Segments. Each Segment was essentially a small HashMap protected by its own ReentrantLock. The key determined which segment to use, and only that segment was locked during writes.
This meant at most 16 threads could write concurrently (one per segment). Reads required acquiring a read lock on the segment. The segment count was fixed at creation time and could not be changed.
// Java 7 ConcurrentHashMap (simplified)
class ConcurrentHashMap<K,V> {
Segment<K,V>[] segments; // default 16 segments
static class Segment<K,V> extends ReentrantLock {
HashEntry<K,V>[] table; // each segment has its own bucket array
V put(K key, int hash, V value) {
lock(); // lock THIS segment only
try {
// find bucket in this segment, insert/replace
} finally {
unlock();
}
}
}
}
// Limitation: fixed 16 segments, entire segment locked for write
How does Java 8+ ConcurrentHashMap work?
Java 8 completely redesigned ConcurrentHashMap. It uses a flat Node[] table – the same structure as HashMap. The key improvements are:
1. Reads are lock-free. The get() method uses volatile reads of the table reference, bucket head, and node values. No locks, no CAS, just volatile semantics.
2. Writes to empty buckets use CAS. Compare-And-Swap is a hardware atomic operation. If the bucket is empty (null), CAS atomically sets it to the new Node.
3. Writes to non-empty buckets use synchronized. The synchronized block locks only the first Node of the specific bucket. Other buckets remain accessible.
package com.bank.demo;
import java.math.BigDecimal;
import java.util.concurrent.ConcurrentHashMap;
public class CHMInternalsDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, BigDecimal> balances = new ConcurrentHashMap<>();
// Write to empty bucket: CAS (no lock)
balances.put("SAV2001", new BigDecimal("150000"));
// Read: volatile read (no lock)
BigDecimal bal = balances.get("SAV2001");
System.out.println("Balance: " + bal);
// Write to non-empty bucket: synchronized on first node
// (if another key hashes to the same bucket)
balances.put("CUR3001", new BigDecimal("75000"));
// Atomic compound operation
balances.merge("SAV2001", new BigDecimal("5000"), BigDecimal::add);
System.out.println("After deposit: " + balances.get("SAV2001"));
}
}
Balance: 150000
After deposit: 155000
How does distributed resize work?
When the table needs to grow, Java 8+ ConcurrentHashMap distributes the migration across multiple threads:
- A new table (2x capacity) is allocated.
- A
transferIndexcounter starts at the old table's length. - Each thread that accesses the map during resize claims a stride of buckets (minimum 16) by atomically decrementing
transferIndex. - Each thread migrates its claimed buckets to the new table.
- When all buckets are migrated, the new table becomes active.
This means N threads can help resize simultaneously, making resize N times faster than a single-threaded approach.
// Pseudocode of distributed resize
while (transferIndex > 0) {
int myStride = Math.max(MIN_TRANSFER_STRIDE, n / ncpu);
int start = CAS(transferIndex, transferIndex - myStride);
for (int i = start; i < start + myStride; i++) {
// migrate bucket i from old table to new table
}
}
How is Java 8 better than Java 7?
| Feature | Java 7 | Java 8+ |
|---|---|---|
| Structure | Segment[] (16 segments) | Node[] (flat table) |
| Read locking | Segment read lock | None (volatile) |
| Write locking | Segment lock | CAS or sync(bucket) |
| Granularity | 1/16 of map | 1/bucket |
| Max concurrency | 16 writers | Number of buckets |
| Resize | Single-threaded | Multi-threaded |
| Treeification | No | Yes (threshold 8) |
| Atomic ops | Limited | Full (compute, merge) |
What are common mistakes?
Mistake: Assuming size() is exact
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// Under concurrent modifications, size() is approximate
int size = map.size(); // may not reflect all concurrent puts
// Use mappingCount() for long return type (avoids int overflow)
long count = map.mappingCount();
Mistake: Blocking in computeIfAbsent
// WRONG: long computation blocks other threads accessing the same key
map.computeIfAbsent("SAV2001", k -> {
Thread.sleep(5000); // blocks bucket lock for 5 seconds!
return loadFromDatabase(k);
});
// CORRECT: compute quickly, load lazily
map.computeIfAbsent("SAV2001", k -> new LazyWrapper(k));
What should a senior engineer know?
- The spread function. ConcurrentHashMap uses spread(int h) which is (h ^ (h >>> 16)) & HASH_BITS. The & HASH_BITS (0x7fffffff) ensures the hash is non-negative because negative hashes are used internally for forwarding nodes during resize.
- ForwardingNode. During resize, migrated buckets are replaced with ForwardingNode objects that point to the new table. When a thread encounters a ForwardingNode during put/get, it redirects to the new table.
- CounterCell array. Instead of a single size counter (which would be a contention point), Java 8 CHM uses a CounterCell[] array. Each thread increments its own cell. size() sums all cells.
- computeIfAbsent deadlock. In rare cases, calling computeIfAbsent recursively for the same key can deadlock because the thread holds the bucket lock and tries to acquire it again.
What are common interview follow-up questions?
- "How are reads lock-free?" – Volatile reads of node values and next pointers.
- "What is the concurrency level in Java 8?" – Effectively the number of buckets (no fixed segment count).
- "How does resize work with multiple threads?" – Each thread claims a stride of buckets via CAS on transferIndex.
- "Is computeIfAbsent atomic?" – Yes, per-key atomic. The function executes at most once for a given key.
Visual Overview
Java 7 vs Java 8 ConcurrentHashMap Java 7: Segment-based ┌─────────────────────────────────────┐ │ Segment[0] Segment[1] ... [15] │ │ ┌─────┐ ┌─────┐ ┌─────┐│ │ │Lock │ │Lock │ │Lock ││ │ │Table│ │Table│ │Table││ │ └─────┘ └─────┘ └─────┘│ │ Max 16 concurrent writers │ └─────────────────────────────────────┘ Java 8: Per-bucket ┌─────────────────────────────────────┐ │ Node[] table │ │ [0] [1] [2] ... [n-1] │ │ ↓ ↓ ↓ ↓ │ │ null Node Node null │ │ ↓ sync ↓ sync │ │ │ │ Empty bucket: CAS (no lock) │ │ Non-empty: sync on first node │ │ Reads: volatile (no lock) │ │ Resize: multi-thread (stride CAS) │ └─────────────────────────────────────┘
Key Takeaway
Java 8 completely redesigned ConcurrentHashMap. The old segment-based design (16 locks) was replaced with per-bucket granularity: reads are lock-free via volatile semantics, writes to empty buckets use CAS, and writes to non-empty buckets synchronize only on the first node of that specific bucket.
The biggest improvement beyond locking is distributed resize. When the table needs to grow, every thread that accesses the map during resize helps migrate buckets. Each thread claims a stride using CAS, migrates those buckets, and moves on. With 8 CPU cores, resize is 8x faster than single-threaded.
In the National Bank of India's real-time trading system, the balance cache uses ConcurrentHashMap with 2 million entries. Java 7's CHM with 16 segments handled 500K operations per second. After upgrading to Java 8's per-bucket design, throughput jumped to 2M operations per second – a 4x improvement from finer granularity and lock-free reads.
Remember one sentence: Java 8 ConcurrentHashMap uses CAS for empty buckets, synchronized per-bucket for non-empty, volatile for reads, and distributed resize – making it the fastest thread-safe map in Java.
---