Java Tutorial
🔍

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

Java
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

Java
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

Java
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.

Java
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}
Java
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

ScenarioHashMapHashtablesynchronizedMapConcurrentHashMap
Single-threaded getO(1)O(1) + lockO(1) + lockO(1)
Multi-threaded getData raceSerialisedSerialisedFully parallel
Single-threaded putO(1) avgO(1) + lockO(1) + lockO(1) avg
Multi-threaded put (diff buckets)Data raceSerialisedSerialisedConcurrent
Multi-threaded put (same bucket)Data raceSerialisedSerialisedPer-bucket lock
Iteration + concurrent writeCMECMECMEWeakly consistent
Atomic compound opsNoNoNoYes
size() accuracyExactExactExactApproximate
Null key/valuekey: 1Neitherkey: 1Neither

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

Java
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

Java
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

Java
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 carefully

Mistake 4 — Using size() to Guard Capacity Limits

Java
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

TopicLink
How HashMap's hash table compares to ConcurrentHashMap's concurrent architectureJava HashMap →
How Java thread safety and synchronisation work at the JVM levelJava Collections Framework →
How the Iterator interface and fail-fast behaviour contrast with weakly consistent iterationJava Iterator →
How Generics make ConcurrentHashMap type-safe for any key and value typeJava Generics →
How Java Streams process ConcurrentHashMap entries with filter, map, and collectJava Streams API →
Java ConcurrentHashMap | DevStackFlow