Java ConcurrentHashMap
Java ConcurrentHashMap
java.util.concurrent.ConcurrentHashMap<K, V> is the thread-safe map that eliminates the scalability bottleneck that Hashtable and Collections.synchronizedMap() create. Both older approaches serialise every operation — reads and writes — on a single global lock, so adding more threads adds more contention, not more throughput. ConcurrentHashMap reads without any lock at all and writes by locking only the individual bucket being modified. The result scales with thread count instead of collapsing under it.
What Is Java ConcurrentHashMap?
ConcurrentHashMap<K, V> is a concrete class in java.util.concurrent that implements ConcurrentMap<K, V>. It delivers the same key-value semantics as HashMap — O(1) average put(), get(), and remove() — while remaining fully safe for concurrent reads and writes from multiple threads simultaneously.
The diagram below shows where ConcurrentHashMap sits relative to other Map options and the thread-safety spectrum.
THREAD-SAFETY SPECTRUM FOR JAVA MAPS:
HashMap ← NOT thread-safe at all
LinkedHashMap ← NOT thread-safe
TreeMap ← NOT thread-safe
Collections.synchronizedMap(new HashMap<>())
← single global lock on every method
← reads AND writes serialised
← 100 reader threads → 99 blocked
Hashtable ← same as synchronizedMap, legacy
← additionally: no null keys or values
ConcurrentHashMap ← lock-free reads (volatile fields)
← bucket-level lock on writes
← 100 reader threads → 100 proceed
← 16 writer threads in different buckets → 16 proceed
ConcurrentSkipListMap ← sorted concurrent map (like TreeMap + ConcurrentHashMap)
KEY FACTS:
Package : java.util.concurrent
Since : Java 1.5
Implements : ConcurrentMap<K,V>, Serializable
Null keys : NOT allowed — NullPointerException immediately
Null values: NOT allowed — NullPointerException immediately
Ordering : no guaranteed iteration order
Thread-safe: YES — reads lock-free, writes per-bucket
Default capacity : 16 buckets
Default load factor: 0.75
Basic Overview — What Changes Between HashMap and ConcurrentHashMap
HASHMAP INTERNALS (not thread-safe):
table[] ← plain Object[] array
Node.val ← plain field
Node.next ← plain reference
Two threads calling put() simultaneously can:
— corrupt a bucket chain (one insertion lost)
— trigger simultaneous resize → corrupt table[] reference
— Java 6 and earlier: create a circular linked-list during resize
causing get() to loop infinitely
CONCURRENTHASHMAP INTERNALS (thread-safe):
table[] ← volatile Node<K,V>[] (volatile ensures visibility across CPUs)
Node.val ← volatile V (writes immediately visible to all threads)
Node.next ← volatile Node<K,V> (chain traversal sees latest structure)
get() path:
Read table[index] — volatile read, no lock
Walk chain reading volatile val fields — no lock
Returns instantly regardless of concurrent writers
put() path — CASE 1: bucket is empty:
CAS (Compare-And-Swap) table[index] from null to newNode
Single atomic CPU instruction — no lock, no contention
If CAS fails (another thread beat us), loop and retry
put() path — CASE 2: bucket already has entries:
synchronized(table[index]) ← locks ONLY this one bucket head node
Walk chain, insert or update
Treeify if chain exceeds 8 nodes
KEY DIFFERENCE: 16 writers can modify 16 different buckets simultaneously.
Hashtable: only 1 writer at a time, regardless of which bucket.
When to Use ConcurrentHashMap
USE ConcurrentHashMap WHEN:
1. A Map is shared across multiple threads and any thread may write
— session registry, permission cache, feature flag store
— any @Service or @Component field that holds a Map
2. High-read, low-write access patterns
— configuration loaded at startup, read on every request
— product catalogue refreshed hourly, served millions of times
3. Atomic compound operations are needed
— putIfAbsent: register a key exactly once, no double-insert race
— computeIfAbsent: lazy-initialise per key, no double-creation
— merge: atomic counter increment without external locks
4. Concurrent iteration is needed
— ConcurrentHashMap.entrySet() never throws CME during iteration
— background eviction threads can removeIf() while readers iterate
CHOOSE HashMap WHEN:
- Completely single-threaded code — HashMap is slightly faster
and uses less memory (no volatile fields, no bucket lock overhead)
CHOOSE Collections.synchronizedMap WHEN:
- Code predates Java 5 (unlikely) or cannot import java.util.concurrent
- Note: even synchronizedMap needs explicit external sync for iteration
and compound operations — ConcurrentHashMap is almost always better
NEVER USE Hashtable IN NEW CODE:
- Single global lock on every method even in single-threaded scenarios
- Does not expose atomic compound operations
- ConcurrentHashMap has been its explicit replacement since Java 5
NEVER USE ConcurrentHashMap WHEN multi-key atomicity is required:
- CHM guarantees atomicity per single key operation
- To atomically transfer a value from keyA to keyB, you need
external synchronisation or a different design — CHM cannot help
How ConcurrentHashMap Works Internally
The Bucket Array — Four Node Types
ConcurrentHashMap uses the same conceptual bucket array as HashMap but with volatile fields and four possible node types that reflect different states.
CONCURRENTHASHMAP BUCKET STATES:
table[i] = null
→ bucket is empty, accept CAS insert
table[i] = Node<K,V>
→ normal linked-list node (same as HashMap)
→ fields: volatile hash, volatile K key, volatile V val, volatile Node next
table[i] = TreeBin
→ wraps a Red-Black tree (treeified after chain > 8 AND table.length >= 64)
→ same Java 8 treeification as HashMap — O(log n) worst-case per bucket
table[i] = ForwardingNode (hash = -1)
→ signals that this bucket is being moved during a resize operation
→ any get() that sees a ForwardingNode follows it to the new table
→ guarantees reads are never blocked even during resize
How Size Tracking Works — LongAdder Cells
HashMap tracks size with a single int size field. In ConcurrentHashMap, incrementing a shared counter from multiple threads creates contention. Java 8 solves this with a distributed approach.
SIZE TRACKING IN CONCURRENTHASHMAP:
baseCount ← a volatile long, used when contention is low
CounterCell[] counterCells ← an array of cells, each with a volatile long
When a write succeeds:
1. Try CAS on baseCount — if succeeds, done
2. If CAS fails (contention), pick a CounterCell based on thread ID
and CAS on that cell instead
size() and mappingCount() sum all cells:
total = baseCount + sum(counterCell[i].value for all cells)
This sum is an ESTIMATE — it may not reflect concurrent inserts
happening at the exact moment size() is called.
WHY THIS MATTERS FOR PRODUCTION CODE:
map.size() == 0 does not mean no one is inserting right now.
map.size() > MAX_CAPACITY check has a race condition.
Never use size() to enforce capacity limits under concurrency.
Use compute() or a separate AtomicInteger if exact counts are needed.
CAS in Detail — The Empty-Bucket Fast Path
SCENARIO: Thread 1 and Thread 2 both want to insert into bucket[3] (empty)
Thread 1:
expected = null
newNode = Node("user:001", sessionA)
casTabAt(table, 3, null, newNode)
CPU executes: LOCK CMPXCHG [table+3*8], null, newNode
→ atomic: IF table[3] == null THEN table[3] = newNode → SUCCESS
Thread 2 (simultaneously):
expected = null
newNode = Node("user:002", sessionB)
casTabAt(table, 3, null, newNode)
→ FAILS: table[3] is no longer null (Thread 1 won)
→ Thread 2 loops back to the top of the put() loop
→ table[3] != null now → takes synchronized(table[3]) path
→ Acquires lock on bucket head, walks chain, appends sessionB
RESULT:
table[3]: Node("user:001",sessionA) → Node("user:002",sessionB) → null
No lock was ever acquired by Thread 1.
Thread 2 only held a lock for the few nanoseconds to append to the chain.
Why Null Is Prohibited
REASON NULL IS FORBIDDEN IN CONCURRENTHASHMAP:
HashMap allows null values. map.get("key") = null means EITHER:
(a) key does not exist in the map, OR
(b) key exists and its value is null
In HashMap (single-threaded), this is disambiguated by:
if (map.containsKey("key")) { ... } ← safe: no thread intervenes
In ConcurrentHashMap (multi-threaded):
value = map.get("key"); // returns null — Thread 2 inserts here
map.containsKey("key"); // now returns true!
// The check-then-act is NOT atomic — race condition
By prohibiting null values entirely:
map.get("key") == null ←→ key is DEFINITELY absent
No containsKey() disambiguation needed.
Code using ConcurrentHashMap is safer by design.
Core Operations with Examples
Thread-Safe Reads and Writes
1// File: ConcurrentHashMapBasicsDemo.java
2
3import java.util.concurrent.ConcurrentHashMap;
4import java.util.concurrent.ExecutorService;
5import java.util.concurrent.Executors;
6import java.util.concurrent.TimeUnit;
7
8public class ConcurrentHashMapBasicsDemo {
9
10 public static void main(String[] args) throws InterruptedException {
11
12 ConcurrentHashMap<String, Integer> pageViews = new ConcurrentHashMap<>();
13
14 // Pre-load some initial entries
15 pageViews.put("/home", 0);
16 pageViews.put("/products", 0);
17 pageViews.put("/checkout", 0);
18
19 System.out.println("Initial: " + pageViews);
20
21 // Simulate 10 threads each incrementing every page 200 times
22 // Using merge() — atomic increment without external locking
23 ExecutorService pool = Executors.newFixedThreadPool(10);
24 for (int t = 0; t < 10; t++) {
25 pool.submit(() -> {
26 for (int i = 0; i < 200; i++) {
27 pageViews.merge("/home", 1, Integer::sum);
28 pageViews.merge("/products", 1, Integer::sum);
29 pageViews.merge("/checkout", 1, Integer::sum);
30 }
31 });
32 }
33 pool.shutdown();
34 pool.awaitTermination(15, TimeUnit.SECONDS);
35
36 // Each page should have exactly 2000 hits: 10 threads × 200 each
37 System.out.println("After 10 threads × 200 hits each:");
38 pageViews.forEach((page, hits) ->
39 System.out.printf(" %-12s %d hits%n", page, hits));
40
41 System.out.println();
42
43 // get() — fully lock-free, reads latest committed value
44 System.out.println("=== get() and containsKey() ===");
45 System.out.println("get(/home) : " + pageViews.get("/home"));
46 System.out.println("get(/missing) : " + pageViews.get("/missing")); // null — key absent
47 System.out.println("containsKey(/home): " + pageViews.containsKey("/home"));
48
49 // getOrDefault — avoid null-check for absent keys
50 System.out.println("getOrDefault(/api): " +
51 pageViews.getOrDefault("/api", 0));
52
53 // remove — returns the value that was removed
54 System.out.println("\n=== remove() ===");
55 System.out.println("remove(/checkout) : " + pageViews.remove("/checkout"));
56 System.out.println("After remove : " + pageViews);
57 }
58}Output:
Initial: {/checkout=0, /home=0, /products=0}
After 10 threads × 200 hits each:
/checkout 2000 hits
/home 2000 hits
/products 2000 hits
=== get() and containsKey() ===
get(/home) : 2000
get(/missing) : null
containsKey(/home): true
getOrDefault(/api): 0
=== remove() ===
remove(/checkout) : 2000
After remove : {/home=2000, /products=2000}
Atomic Compound Operations — The Core Advantage
1// File: ConcurrentHashMapAtomicOpsDemo.java
2
3import java.util.List;
4import java.util.concurrent.ConcurrentHashMap;
5import java.util.concurrent.CopyOnWriteArrayList;
6
7public class ConcurrentHashMapAtomicOpsDemo {
8
9 public static void main(String[] args) {
10
11 // putIfAbsent — atomic first-write-wins: no race between check and put
12 System.out.println("=== putIfAbsent — register once, atomically ===");
13 ConcurrentHashMap<String, String> idempotencyKeys = new ConcurrentHashMap<>();
14
15 // Returns null when key was absent (insertion succeeded)
16 String prev1 = idempotencyKeys.putIfAbsent("TXN-9921", "processed");
17 System.out.println("putIfAbsent TXN-9921 first : " + prev1); // null
18
19 // Returns existing value when key already present (insertion rejected)
20 String prev2 = idempotencyKeys.putIfAbsent("TXN-9921", "duplicate-attempt");
21 System.out.println("putIfAbsent TXN-9921 second : " + prev2); // processed
22
23 System.out.println("Map: " + idempotencyKeys);
24
25 System.out.println();
26
27 // computeIfAbsent — lazy-initialise per key with one atomic operation
28 System.out.println("=== computeIfAbsent — group-by, thread-safe ===");
29 ConcurrentHashMap<String, List<String>> groupedEvents = new ConcurrentHashMap<>();
30 String[][] events = {
31 {"PAYMENT", "txn-001"}, {"LOGIN", "user-A"},
32 {"PAYMENT", "txn-002"}, {"LOGOUT", "user-B"},
33 {"LOGIN", "user-C"}, {"PAYMENT", "txn-003"}
34 };
35 for (String[] event : events) {
36 // computeIfAbsent creates the list only once per category — atomic
37 groupedEvents
38 .computeIfAbsent(event[0], k -> new CopyOnWriteArrayList<>())
39 .add(event[1]);
40 }
41 groupedEvents.forEach((category, ids) ->
42 System.out.println(" " + category + ": " + ids));
43
44 System.out.println();
45
46 // merge — atomic value accumulation across threads
47 System.out.println("=== merge — atomic counter pattern ===");
48 ConcurrentHashMap<String, Long> errorCounts = new ConcurrentHashMap<>();
49 String[] errors = {"DB_TIMEOUT","AUTH_FAIL","DB_TIMEOUT","NPE","AUTH_FAIL","DB_TIMEOUT"};
50 for (String error : errors) {
51 errorCounts.merge(error, 1L, Long::sum); // atomic: insert 1 or add 1 to existing
52 }
53 System.out.println("Error counts: " + errorCounts);
54
55 System.out.println();
56
57 // compute — read-modify-write in one atomic step
58 System.out.println("=== compute — atomic stock management ===");
59 ConcurrentHashMap<String, Integer> inventory = new ConcurrentHashMap<>();
60 inventory.put("SKU-001", 100);
61
62 // Deduct 5 units — compute sees current value and returns new value
63 inventory.compute("SKU-001", (sku, qty) -> qty == null ? 0 : qty - 5);
64 System.out.println("After -5 units: SKU-001 = " + inventory.get("SKU-001"));
65
66 // compute returning null removes the key
67 inventory.compute("SKU-001", (sku, qty) -> (qty != null && qty > 0) ? qty : null);
68 System.out.println("Still present (qty > 0): " + inventory.containsKey("SKU-001"));
69
70 System.out.println();
71
72 // replace — conditional update (optimistic locking pattern)
73 System.out.println("=== replace — optimistic update ===");
74 ConcurrentHashMap<String, String> statusMap = new ConcurrentHashMap<>();
75 statusMap.put("order-001", "PENDING");
76
77 // Replace only if the current value matches — no lost-update race
78 boolean updated = statusMap.replace("order-001", "PENDING", "PROCESSING");
79 System.out.println("replace PENDING→PROCESSING: " + updated); // true
80
81 boolean staleUpdate = statusMap.replace("order-001", "PENDING", "PROCESSING");
82 System.out.println("replace PENDING→PROCESSING again: " + staleUpdate); // false — already changed
83 System.out.println("Current status: " + statusMap.get("order-001"));
84 }
85}Output:
=== putIfAbsent — register once, atomically ===
putIfAbsent TXN-9921 first : null
putIfAbsent TXN-9921 second : processed
Map: {TXN-9921=processed}
=== computeIfAbsent — group-by, thread-safe ===
PAYMENT: [txn-001, txn-002, txn-003]
LOGIN: [user-A, user-C]
LOGOUT: [user-B]
=== merge — atomic counter pattern ===
Error counts: {DB_TIMEOUT=3, AUTH_FAIL=2, NPE=1}
=== compute — atomic stock management ===
After -5 units: SKU-001 = 95
Still present (qty > 0): true
=== optimistic update ===
replace PENDING→PROCESSING: true
replace PENDING→PROCESSING again: false
Current status: PROCESSING
Weakly Consistent Iteration and Bulk Operations
1// File: ConcurrentHashMapIterationDemo.java
2
3import java.util.Map;
4import java.util.Set;
5import java.util.concurrent.ConcurrentHashMap;
6
7public class ConcurrentHashMapIterationDemo {
8
9 public static void main(String[] args) throws InterruptedException {
10
11 ConcurrentHashMap<String, Double> prices = new ConcurrentHashMap<>();
12 prices.put("Laptop", 49999.0);
13 prices.put("Mouse", 999.0);
14 prices.put("Keyboard", 2499.0);
15 prices.put("Monitor", 18999.0);
16 prices.put("Webcam", 3499.0);
17
18 // for-each — never throws ConcurrentModificationException
19 System.out.println("=== for-each — weakly consistent, no CME ===");
20 for (Map.Entry<String, Double> entry : prices.entrySet()) {
21 System.out.printf(" %-12s Rs.%7.2f%n", entry.getKey(), entry.getValue());
22 // Structural modification during iteration — no CME!
23 if ("Mouse".equals(entry.getKey())) {
24 prices.put("Trackpad", 4999.0); // may or may not appear this iteration
25 }
26 }
27 System.out.println("Map now has " + prices.size() + " entries (Trackpad added)");
28
29 System.out.println();
30
31 // Safe conditional bulk removal
32 System.out.println("=== entrySet().removeIf() — safe concurrent removal ===");
33 prices.entrySet().removeIf(entry -> entry.getValue() < 2000.0);
34 System.out.println("After removing items below Rs.2000: " + prices.keySet());
35
36 System.out.println();
37
38 // ConcurrentHashMap.newKeySet() — thread-safe Set backed by ConcurrentHashMap
39 System.out.println("=== newKeySet() — thread-safe Set ===");
40 Set<String> activeUsers = ConcurrentHashMap.newKeySet();
41 activeUsers.add("user-priya");
42 activeUsers.add("user-rohan");
43 activeUsers.add("user-ananya");
44 activeUsers.add("user-priya"); // duplicate — rejected by Set semantics
45 System.out.println("Active users: " + activeUsers);
46 System.out.println("Size: " + activeUsers.size()); // 3, not 4
47
48 System.out.println();
49
50 // forEach with parallelism threshold — parallel bulk operation
51 System.out.println("=== forEach bulk operation ===");
52 prices.forEach((product, price) ->
53 System.out.printf(" %-12s Rs.%7.2f%n", product, price));
54 }
55}Output:
=== for-each — weakly consistent, no CME ===
Laptop Rs. 49999.00
Mouse Rs. 999.00
Keyboard Rs. 2499.00
Monitor Rs. 18999.00
Webcam Rs. 3499.00
Map now has 6 entries (Trackpad added)
=== entrySet().removeIf() — safe concurrent removal ===
After removing items below Rs.2000: [Laptop, Keyboard, Monitor, Webcam, Trackpad]
=== newKeySet() — thread-safe Set ===
Active users: [user-priya, user-rohan, user-ananya]
Size: 3
=== forEach bulk operation ===
Laptop Rs. 49999.00
Keyboard Rs. 2499.00
Monitor Rs. 18999.00
Webcam Rs. 3499.00
Trackpad Rs. 4999.00
Real-World Example — Swiggy Real-Time Delivery Metrics
A delivery platform like Swiggy tracks live metrics across thousands of concurrent delivery agents — active orders per zone, delivery success rates, and error counts — while simultaneously serving these metrics to a dashboard that reads them every second. ConcurrentHashMap handles concurrent agent updates on write threads and dashboard reads on read threads without any external synchronisation.
1// File: ZoneMetrics.java
2
3public class ZoneMetrics {
4
5 private final String zoneName;
6 private volatile int activeOrders;
7 private volatile long totalDelivered;
8 private volatile long totalFailed;
9
10 public ZoneMetrics(String zoneName) {
11 this.zoneName = zoneName;
12 this.activeOrders = 0;
13 this.totalDelivered = 0;
14 this.totalFailed = 0;
15 }
16
17 public String getZoneName() { return zoneName; }
18 public int getActiveOrders() { return activeOrders; }
19 public long getTotalDelivered(){ return totalDelivered; }
20 public long getTotalFailed() { return totalFailed; }
21
22 public void orderAssigned() { activeOrders++; }
23 public void orderDelivered() { activeOrders--; totalDelivered++; }
24 public void orderFailed() { activeOrders--; totalFailed++; }
25
26 public double successRate() {
27 long total = totalDelivered + totalFailed;
28 return total == 0 ? 100.0 : (totalDelivered * 100.0 / total);
29 }
30
31 @Override
32 public String toString() {
33 return String.format("%-14s active=%-4d delivered=%-5d failed=%-3d rate=%.1f%%",
34 zoneName, activeOrders, totalDelivered, totalFailed, successRate());
35 }
36}1// File: DeliveryMetricsDashboard.java
2
3import java.util.concurrent.ConcurrentHashMap;
4import java.util.concurrent.Executors;
5import java.util.concurrent.ScheduledExecutorService;
6import java.util.concurrent.TimeUnit;
7import java.util.concurrent.atomic.AtomicInteger;
8
9public class DeliveryMetricsDashboard {
10
11 // computeIfAbsent ensures ZoneMetrics is created exactly once per zone
12 private final ConcurrentHashMap<String, ZoneMetrics> zoneStats =
13 new ConcurrentHashMap<>();
14
15 private final AtomicInteger totalEventsProcessed = new AtomicInteger();
16
17 // Called by agent update threads — fully concurrent
18 public void recordAssignment(String zone) {
19 zoneStats.computeIfAbsent(zone, ZoneMetrics::new).orderAssigned();
20 totalEventsProcessed.incrementAndGet();
21 }
22
23 public void recordDelivery(String zone) {
24 zoneStats.computeIfAbsent(zone, ZoneMetrics::new).orderDelivered();
25 totalEventsProcessed.incrementAndGet();
26 }
27
28 public void recordFailure(String zone) {
29 zoneStats.computeIfAbsent(zone, ZoneMetrics::new).orderFailed();
30 totalEventsProcessed.incrementAndGet();
31 }
32
33 // Called by dashboard read thread — lock-free iteration
34 public void printDashboard() {
35 System.out.println("=".repeat(72));
36 System.out.println(" SWIGGY LIVE DELIVERY METRICS [events: " +
37 totalEventsProcessed.get() + "]");
38 System.out.println("=".repeat(72));
39 // Weakly consistent iteration — reads all committed values
40 zoneStats.values().stream()
41 .sorted((a, b) -> a.getZoneName().compareTo(b.getZoneName()))
42 .forEach(m -> System.out.println(" " + m));
43 System.out.println("=".repeat(72));
44 }
45
46 public static void main(String[] args) throws InterruptedException {
47
48 DeliveryMetricsDashboard dashboard = new DeliveryMetricsDashboard();
49
50 // Simulate agent update threads (writes)
51 ScheduledExecutorService agentPool = Executors.newScheduledThreadPool(6);
52
53 // Koramangala zone — heavy traffic
54 agentPool.submit(() -> {
55 for (int i = 0; i < 50; i++) {
56 dashboard.recordAssignment("Koramangala");
57 dashboard.recordDelivery("Koramangala");
58 }
59 });
60
61 // Indiranagar zone — medium traffic
62 agentPool.submit(() -> {
63 for (int i = 0; i < 30; i++) {
64 dashboard.recordAssignment("Indiranagar");
65 if (i % 5 == 0) dashboard.recordFailure("Indiranagar");
66 else dashboard.recordDelivery("Indiranagar");
67 }
68 });
69
70 // HSR Layout zone — light traffic, some failures
71 agentPool.submit(() -> {
72 for (int i = 0; i < 20; i++) {
73 dashboard.recordAssignment("HSR Layout");
74 if (i % 4 == 0) dashboard.recordFailure("HSR Layout");
75 else dashboard.recordDelivery("HSR Layout");
76 }
77 });
78
79 // Whitefield zone — new zone, few orders
80 agentPool.submit(() -> {
81 for (int i = 0; i < 10; i++) {
82 dashboard.recordAssignment("Whitefield");
83 dashboard.recordDelivery("Whitefield");
84 }
85 });
86
87 agentPool.shutdown();
88 agentPool.awaitTermination(5, TimeUnit.SECONDS);
89
90 // Dashboard snapshot read — concurrent, no CME
91 dashboard.printDashboard();
92
93 System.out.println();
94
95 // Query individual zone — O(1) lock-free read
96 ZoneMetrics km = dashboard.zoneStats.get("Koramangala");
97 if (km != null) {
98 System.out.println("Koramangala success rate: " +
99 String.format("%.1f%%", km.successRate()));
100 }
101
102 // Bulk operation — find zones with active orders remaining
103 System.out.println("\nZones with active orders:");
104 dashboard.zoneStats.forEach((zone, metrics) -> {
105 if (metrics.getActiveOrders() > 0) {
106 System.out.println(" " + zone + ": " + metrics.getActiveOrders() + " active");
107 }
108 });
109 }
110}Output:
========================================================================
SWIGGY LIVE DELIVERY METRICS [events: 110]
========================================================================
HSR Layout active=0 delivered=15 failed=5 rate=75.0%
Indiranagar active=0 delivered=24 failed=6 rate=80.0%
Koramangala active=0 delivered=50 failed=0 rate=100.0%
Whitefield active=0 delivered=10 failed=0 rate=100.0%
========================================================================
Koramangala success rate: 100.0%
Zones with active orders:
Performance Considerations
| Scenario | HashMap | Hashtable | synchronizedMap | ConcurrentHashMap |
|---|---|---|---|---|
| Single-threaded get | O(1) | O(1) + lock | O(1) + lock | O(1) |
| Multi-threaded get | Data race | Serialised | Serialised | Fully parallel |
| Single-threaded put | O(1) avg | O(1) + lock | O(1) + lock | O(1) avg |
| Multi-threaded put (diff buckets) | Data race | Serialised | Serialised | Concurrent |
| Multi-threaded put (same bucket) | Data race | Serialised | Serialised | Per-bucket lock |
| Iteration + concurrent write | CME | CME | CME | Weakly consistent |
| Atomic compound ops | No | No | No | Yes |
| size() accuracy | Exact | Exact | Exact | Approximate |
| Null key/value | key: 1 | Neither | key: 1 | Neither |
Lock-free reads scale linearly: At 1,000 concurrent reader threads on a ConcurrentHashMap, all 1,000 proceed without any contention. On Hashtable or synchronizedMap, 999 block waiting for the one permitted reader to finish. This is the critical difference for read-heavy production caches.
Write throughput scales with bucket count: With default capacity 16, up to 16 threads can write to 16 different buckets simultaneously. At higher concurrency, pre-sizing to a larger capacity (new ConcurrentHashMap<>(capacity)) spreads writes across more buckets and reduces per-bucket contention.
Memory overhead: ConcurrentHashMap entries use volatile fields, which consume additional memory for the CPU cache coherence protocol. The size counter uses LongAdder-style distributed cells under high concurrency. Total memory per entry is roughly 1.5-2× that of HashMap. For maps holding tens of millions of entries, this overhead is significant.
Best Practices
Always use atomic compound operations — never check-then-act. map.putIfAbsent(key, value) is a single atomic operation. if (!map.containsKey(key)) map.put(key, value) is two operations separated by a window where another thread can insert — a classic race condition in any concurrent code. Similarly, map.computeIfAbsent(key, k -> new ArrayList<>()) creates the value exactly once per key even when 100 threads call it concurrently. map.merge(key, 1, Integer::sum) is the correct atomic counter pattern.
Never use size() for capacity enforcement logic. size() returns an estimate that can lag behind concurrent inserts and removes. The pattern if (map.size() < MAX_SIZE) { map.put(key, value); } has a race condition — two threads can both see size below the limit and both insert, exceeding it. Enforce limits inside a compute() that checks and acts atomically within the same lambda.
Declare the variable as ConcurrentMap<K, V> rather than ConcurrentHashMap<K, V>. Using the interface type gives you the putIfAbsent(), compute(), computeIfAbsent(), computeIfPresent(), merge(), and replace() atomic operations without coupling callers to the concrete class. Swapping to ConcurrentSkipListMap for sorted concurrent access requires no signature changes when the interface is declared.
Use ConcurrentHashMap.newKeySet() for thread-safe Sets. Collections.newSetFromMap(new ConcurrentHashMap<>()) and ConcurrentHashMap.newKeySet() both produce a Set<E> backed by ConcurrentHashMap. The newKeySet() factory is cleaner and produces a KeySetView<E, Boolean> that inherits all the concurrent read/write guarantees. This is the correct replacement for a Collections.synchronizedSet(new HashSet<>()).
Common Mistakes
Mistake 1 — Non-Atomic Check-Then-Act Pattern
1ConcurrentHashMap<String, Integer> counters = new ConcurrentHashMap<>();
2
3// WRONG — gap between get() and put() is a race window
4Integer current = counters.get("visits");
5counters.put("visits", current == null ? 1 : current + 1);
6// Two threads can both read null and both put 1 → net result: 1 instead of 2
7
8// CORRECT — single atomic operation, no race
9counters.merge("visits", 1, Integer::sum);
10
11// ALSO CORRECT using compute():
12counters.compute("visits", (key, val) -> val == null ? 1 : val + 1);Mistake 2 — Inserting null Key or Value
1ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
2
3// BOTH throw NullPointerException immediately
4cache.put(null, "value"); // NullPointerException
5cache.put("key", null); // NullPointerException
6
7// CORRECT — use non-null sentinels or Optional as value type
8cache.put("key", ""); // empty string as absent-value signal
9// OR:
10ConcurrentHashMap<String, java.util.Optional<String>> safeCache = new ConcurrentHashMap<>();
11safeCache.put("key", java.util.Optional.empty()); // explicit "no value present"Mistake 3 — Wrapping ConcurrentHashMap in synchronizedMap
1// WRONG — adds a global lock on top of fine-grained locks
2// Destroys every scalability benefit of ConcurrentHashMap
3Map<String, String> wrapped =
4 java.util.Collections.synchronizedMap(new ConcurrentHashMap<>());
5
6// WRONG — locking externally on individual method calls
7synchronized (myConcurrentMap) {
8 myConcurrentMap.put(key, value); // pointless — put() is already atomic
9}
10
11// CORRECT — use directly, no wrapping needed
12myConcurrentMap.put(key, value); // thread-safe, no annotation needed
13
14// External sync is ONLY needed for truly multi-key compound operations:
15// synchronized(lock) { read keyA; read keyB; act on both } — use carefullyMistake 4 — Using size() to Guard Capacity Limits
1ConcurrentHashMap<String, String> boundedCache = new ConcurrentHashMap<>();
2static final int MAX = 1000;
3
4// WRONG — race condition: two threads both see size() < 1000 and both insert
5if (boundedCache.size() < MAX) {
6 boundedCache.put(key, value); // may exceed MAX
7}
8
9// CORRECT — atomic size enforcement inside compute()
10boundedCache.compute(key, (k, existing) -> {
11 if (existing != null) return value; // update always allowed
12 if (boundedCache.size() >= MAX) return null; // reject new key at capacity
13 return value;
14});Interview Questions
Q1. What is ConcurrentHashMap and why was it introduced?
ConcurrentHashMap is a thread-safe Map in java.util.concurrent that replaced Hashtable as the correct solution for concurrent map access. Hashtable synchronises every method on a single global lock — every get() and put() from any thread acquires the same lock, serialising all operations. Under 100 concurrent threads, only one proceeds at a time while the other 99 wait. ConcurrentHashMap uses lock-free reads via volatile fields and per-bucket synchronisation for writes, allowing many threads to read simultaneously and multiple writers to operate in different buckets concurrently. It also provides atomic compound operations — putIfAbsent(), computeIfAbsent(), merge() — that Hashtable does not.
Q2. How does ConcurrentHashMap achieve lock-free reads?
ConcurrentHashMap marks its internal table[] array as volatile Node<K,V>[], and each node's val and next fields as volatile. In Java's memory model, a volatile read always returns the most recently written value — no CPU cache can return a stale copy. When get(key) executes, it reads table[bucketIndex] (volatile) and then walks the chain reading node.val (volatile), without acquiring any lock. A concurrent put() that completes before the get() starts is guaranteed to be visible. A put() still in progress at the moment of get() may or may not be visible — this is the weakly consistent read guarantee.
Q3. What is the CAS operation and where does ConcurrentHashMap use it?
CAS (Compare-And-Swap) is a single hardware instruction that atomically checks whether a memory location contains an expected value and replaces it with a new value only if the check succeeds. It either succeeds and updates the memory, or fails and returns — no spin, no lock. ConcurrentHashMap uses CAS to insert into an empty bucket: casTabAt(table, bucketIndex, null, newNode) — atomically place newNode only if that slot is currently null. If two threads race to insert into the same empty bucket, exactly one CAS wins and the loser retries through the synchronized(bucketHead) path. This makes the common case of inserting into uncrowded buckets entirely lock-free.
Q4. Why does ConcurrentHashMap not allow null keys or values?
Prohibiting null values eliminates an entire class of race conditions. In HashMap, get(key) returning null is ambiguous — it could mean the key is absent, or the key maps to null. Disambiguation requires containsKey(), but in concurrent code containsKey() and get() are two separate non-atomic operations — another thread can insert or remove between them. By prohibiting null values entirely, get() returning null unambiguously means the key is absent, removing the need for a second round-trip check. The same reasoning applies to null keys: the null-key handling in HashMap goes to bucket 0, but the ConcurrentHashMap bucket-locking design assumes non-null key comparisons.
Q5. What is the difference between compute(), computeIfAbsent(), and merge() in ConcurrentHashMap?
All three are atomic compound operations that avoid external synchronisation. computeIfAbsent(key, mappingFn) runs the mapping function only if the key is absent, atomically stores the result, and returns it — the function executes exactly once per key even under 100 concurrent callers. compute(key, remappingFn) always runs the function with the current value (or null if absent) and stores the return value, removing the key if null is returned. merge(key, value, mergeFn) stores value when the key is absent; when the key exists, calls mergeFn(existingValue, value) atomically and stores the result. merge(key, 1, Integer::sum) is the canonical thread-safe counter pattern.
Q6. What is weakly consistent iteration in ConcurrentHashMap?
A ConcurrentHashMap iterator never throws ConcurrentModificationException — it can be used safely even while other threads add or remove entries. The iterator reflects the state of the map at iterator creation time for entries that existed then. Entries added after the iterator was created may or may not be visited; entries removed after creation may or may not still appear. This "weakly consistent" contract contrasts with HashMap's fail-fast iterator (CME on any structural change) and CopyOnWriteArrayList's snapshot iterator (always sees exactly the state at creation). For monitoring, logging, and eviction sweeps, weakly consistent iteration is sufficient and the cost is zero — no snapshot copy is made.
FAQs
Is it safe to iterate ConcurrentHashMap while another thread modifies it?
Yes. ConcurrentHashMap's iterator is weakly consistent — it never throws ConcurrentModificationException regardless of concurrent modifications. The iteration reflects some partially consistent view: elements that existed at iteration start are visited; elements added or removed during iteration may or may not appear. For most monitoring and eviction use cases this is acceptable. If snapshot consistency is required, copy the map first: new HashMap<>(concurrentHashMap).
What is the difference between size() and mappingCount() in ConcurrentHashMap?
Both return an estimate of the number of entries and neither is exact under active concurrent modification. size() returns an int — capped at Integer.MAX_VALUE. mappingCount() returns a long — appropriate for maps that could theoretically exceed two billion entries. For business logic that depends on an exact count under concurrent access, maintain a separate AtomicLong counter alongside the map.
Can ConcurrentHashMap be used as a simple thread-safe cache?
Yes for basic caching — computeIfAbsent() loads values lazily and exactly once per key, remove() evicts entries, and entrySet().removeIf() performs bulk eviction safely. For production caches with TTL expiry, LRU eviction, statistics, and maximum size enforcement, consider the Caffeine library instead. It is built on ConcurrentHashMap internals but adds all cache-specific semantics.
What replaced the Segment array from Java 7 ConcurrentHashMap?
Java 7 divided the bucket array into 16 fixed Segment objects each with a ReentrantLock. The maximum true concurrency was the number of segments. Java 8 removed segments entirely and replaced them with bucket-level synchronized blocks combined with CAS for empty-bucket inserts. The effective concurrency level is now the number of occupied buckets — much higher than 16 — and the implementation is simpler, faster, and more adaptive.
How do I create a thread-safe Set using ConcurrentHashMap?
ConcurrentHashMap.newKeySet() returns a Set<E> backed by ConcurrentHashMap with all concurrency guarantees. add(), remove(), and contains() are all thread-safe. Iteration is weakly consistent. This is the correct replacement for Collections.synchronizedSet(new HashSet<>()).
Is ConcurrentHashMap safe for multi-key atomic operations?
No. ConcurrentHashMap guarantees atomicity only for single-key operations. The atomic methods — putIfAbsent, compute, merge, replace — are each atomic on their single key. There is no built-in way to atomically read key A and key B together or atomically move a value from A to B. For multi-key atomicity, use external synchronisation on a shared lock, or design the data model to avoid cross-key dependencies.
Summary
ConcurrentHashMap<K, V> achieves thread-safe O(1) map operations without a global lock. Reads are lock-free because every relevant field is volatile — no thread sees a stale value. Writes lock only the single bucket being modified. Empty-bucket inserts use CAS — a single hardware instruction, no lock at all. Multiple threads writing to different buckets never contend with each other.
Four rules govern correct production use: never insert null keys or values, never use size() for capacity enforcement (it is approximate), always prefer atomic compound operations over check-then-act patterns, and never wrap ConcurrentHashMap in synchronizedMap or add external locks to individual operations.
For interviews: explain lock-free reads via volatile fields, describe the CAS empty-bucket insert and the synchronized full-bucket path, explain why null is prohibited (the race between get and containsKey), describe weakly consistent iteration versus fail-fast, and walk through computeIfAbsent() as the correct thread-safe lazy-init pattern. These questions cover fresher rounds at TCS through senior architecture discussions at Flipkart, Razorpay, and Swiggy.
What to Read Next
| Topic | Link |
|---|---|
| How HashMap's hash table compares to ConcurrentHashMap's concurrent architecture | Java HashMap → |
| How Java thread safety and synchronisation work at the JVM level | Java Collections Framework → |
| How the Iterator interface and fail-fast behaviour contrast with weakly consistent iteration | Java Iterator → |
| How Generics make ConcurrentHashMap type-safe for any key and value type | Java Generics → |
| How Java Streams process ConcurrentHashMap entries with filter, map, and collect | Java Streams API → |