RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
HardJava#357 min readJul 11, 2026

ConcurrentHashMap internals.

practice
learning
Practice modeTest yourself instead of reading straight through

Interviewers love this one because it quickly shows whether you know the modern Java design or just the old folklore.

Question: ConcurrentHashMap internals.

Answer: ConcurrentHashMap is Java’s thread-safe hash table for shared access with high concurrency. In modern Java, reads are mostly lock-free, writes only lock a single bucket when needed, and collisions are handled with linked lists or tree bins. It does not allow null keys or values, and its iterators are weakly consistent, which means they do not throw ConcurrentModificationException while the map is changing.

Interview-Ready Answer: I’d say that ConcurrentHashMap is a highly concurrent hash table. In Java 8 and later, it no longer uses the old segment-based design; instead, reads are usually lock-free, empty-bucket inserts use CAS, and contended updates synchronize only on the affected bin. It also uses tree bins for heavy collisions, supports safe concurrent updates, and rejects null so null can keep its meaning as “not present.”

🧠 Memory Map
Memory map — visual summary of this topic

What it is

ConcurrentHashMap is a map built for many threads reading and writing at the same time. The practical goal is simple: avoid one giant lock around the whole map, because that makes every thread wait for everyone else.

Older Java versions used segments (small locked sub-maps). Java 8 removed that design and switched to bin-level control, which is lighter and scales better under read-heavy and mixed workloads.

How it works under the hood

  1. Hash spreading: the key’s hash is mixed so bits are distributed better across the table. The table size is always a power of two, so the map can quickly turn a hash into an array index.
  2. Fast read path: get usually just reads a volatile table slot and walks a short chain. Volatile means the read sees up-to-date memory without taking a lock.
  3. Empty-bin insert: if a bucket is empty, the map uses CAS (compare-and-swap, an atomic CPU operation) to install the first node without locking.
  4. Contended update: if a bucket already has entries, the map synchronizes only on that bin’s first node. That means two unrelated keys in different bins can still proceed in parallel.
  5. Collision handling: a bucket starts as a linked list. If collisions get heavy, and the bin grows beyond 8 nodes while the table is at least 64 entries wide, it is treeified into a red-black tree. If it later shrinks below 6, it can be converted back to a list.
  6. Resize help: when the map grows past its threshold, multiple threads can help transfer bins to the new larger table. Moved bins are marked so other threads know where to look during the transition.

Why it scales

There is no single global lock for normal operations. Reads are very cheap, writes only block a narrow region, and resizing is cooperative. That is why ConcurrentHashMap performs much better than a fully synchronized map when the workload has lots of parallel access.

Important internals and gotchas

  • No null keys or values: null is reserved to mean “absent,” which keeps reads simple and avoids ambiguity during races.
  • Weakly consistent iteration: iterators do not fail fast. They may reflect some updates that happen during iteration, but they are not a frozen snapshot.
  • Size is not a perfect moment-by-moment truth: under heavy mutation, size() can be expensive or only approximate in effect. For large concurrent counters, prefer mappingCount() or design around approximate metrics.
  • Compound actions need care: a single method like putIfAbsent or computeIfAbsent is atomic for that key, but a multi-step business rule across several keys still needs extra synchronization.
  • Constructors in Java 8+: concurrencyLevel is only a sizing hint now; it does not create segments anymore.

Comparison with common alternatives

TypeLockingNullsIterationTypical use
ConcurrentHashMapBin-levelNoWeakly consistentShared concurrent state
HashtableWhole mapNoFail-fast-ish legacy behaviorOld code, rarely ideal
synchronizedMapWhole map via wrapperDepends on backing mapMust synchronize manuallySimple low-contention cases

Performance notes

Average lookup and update are typically close to O(1). In the worst case, a bad hash distribution can still make a bucket long, but tree bins improve that to roughly O(log n) inside the hot bucket. Space overhead is a bit higher than a plain HashMap because of concurrency bookkeeping, counter cells, and node structures.

Memory hook: think of it like a mall with many checkout lanes, not one giant cashier. Most shoppers move freely, and only the lane they touch gets briefly managed.

Real-world story

Imagine a checkout service in an e-commerce platform that tracks how many times each promo code has been reserved in the last minute. Many request threads update the same map at once, and a metrics thread reads it to show live usage.

The team first used a plain HashMap, then wrapped a few methods with synchronized and thought they were safe. Under load, counts were lost, the same coupon got oversold, and the dashboard sometimes threw ConcurrentModificationException while iterating. After switching to ConcurrentHashMap plus LongAdder, updates became scalable and the counts stopped drifting under contention.

What goes wrong when misunderstood: users see promo rejections that should have been accepted, or worse, too many redemptions slip through. In logs you might see inconsistent counts, stale dashboards, and threads waiting on a single lock if the team used a fully synchronized map. The outage symptom is not usually a crash; it is silent data corruption or slow throughput, which is harder to spot and more dangerous in production.

Java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAdder;

public class ConcurrentHashMapInternalsDemo {
    public static void main(String[] args) throws InterruptedException {
        // A shared map for concurrent counting.
        // LongAdder is a good match for hot counters because many threads can increment it with less contention.
        ConcurrentHashMap<String, LongAdder> counts = new ConcurrentHashMap<>();

        int threads = 6;
        int iterationsPerThread = 50_000;

        ExecutorService pool = Executors.newFixedThreadPool(threads);
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch done = new CountDownLatch(threads);

        for (int i = 0; i < threads; i++) {
            pool.execute(() -> {
                try {
                    start.await();

                    for (int j = 0; j < iterationsPerThread; j++) {
                        // computeIfAbsent is atomic for this key: one counter object gets installed and reused.
                        counts.computeIfAbsent("java", k -> new LongAdder()).increment();

                        // A second key shows that the map can hold multiple independent hot spots.
                        if ((j & 15_383) == 0) {
                            counts.computeIfAbsent("concurrency", k -> new LongAdder()).increment();
                        }
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();
                }
            });
        }

        start.countDown();
        done.await();
        pool.shutdown();

        System.out.println("java count       = " + counts.get("java").sum());
        System.out.println("concurrency count = " + counts.get("concurrency").sum());
        System.out.println("mappingCount      = " + counts.mappingCount());

        // Edge case: ConcurrentHashMap rejects nulls by design.
        // That is intentional because null is used as a special 'absent' marker in many concurrent algorithms.
        try {
            counts.put(null, new LongAdder());
        } catch (NullPointerException ex) {
            System.out.println("null key rejected: " + ex.getClass().getSimpleName());
        }

        try {
            counts.put("bad", null);
        } catch (NullPointerException ex) {
            System.out.println("null value rejected: " + ex.getClass().getSimpleName());
        }

        // At this point the map is safe for shared access, but compound business rules still need care.
        // For example, checking one key and then updating another is not a single atomic transaction.
    }
}

Follow-up & Tricky Questions:

  • How does resizing work with multiple threads? Threads do not stop the world. They help transfer bins to the new table, which spreads the cost and avoids one long resize pause.
  • Why are get operations usually lock-free? Reads rely on volatile visibility and linked traversal, so most lookups do not need to block. That is one big reason the map scales well for read-heavy workloads.
  • What is the purpose of tree bins? They protect the map from bad collision chains. Once a bucket gets too long, switching to a tree makes lookup inside that bucket much faster than walking a long list.
  • Why use LongAdder with ConcurrentHashMap? The map gives safe key concurrency, and LongAdder reduces contention on hot counters. Together they are a common pattern for high-throughput metrics.
  • Is concurrencyLevel still a tuning knob? In Java 8+, it is mainly a sizing hint for the initial table, not a segment count. Many candidates remember the old behavior and answer as if segments still exist.
  • Tricky: Are iterators fail-fast? No. They are weakly consistent, so they do not throw ConcurrentModificationException just because another thread updates the map.
  • Tricky: Can I rely on size() during concurrent writes? Not as a precise synchronization point. It is fine for approximate reporting, but not for enforcing a strict business invariant under heavy mutation.
  • Tricky: Does ConcurrentHashMap make a multi-step operation atomic? No. A single method call may be atomic for one key, but a sequence like check-then-act across several keys still needs extra coordination.

Common Mistakes:

  • Thinking it still uses segments: That was the old JDK 7 design. Modern Java uses per-bin control, CAS, and cooperative resizing.
  • Using null to mean missing data: This map forbids null, so missing must be represented with a different value or an explicit check.
  • Assuming iteration is a snapshot: It is not. You may see some updates, and you may miss others, but you will not get fail-fast behavior.
  • Putting slow work inside mapping functions: computeIfAbsent and related methods may hold internal bin control, so blocking I/O there can stall other threads.

Memory Hook: picture a supermarket with many short checkout lanes. Empty lane? Slide in with a quick atomic move. Busy lane? Briefly stand at that lane only. Bad collision? Turn the aisle into a line of labeled shelves, then split the crowd when the store gets crowded.

Cheat Sheet:

  • Java 8+ CHM removed segment locks.
  • Reads are mostly lock-free; writes lock one bin or use CAS.
  • Treeify at about 8 nodes, untreeify below about 6, treeify only when table is at least 64.
  • No null keys or values.
  • Iterators are weakly consistent, not fail-fast.
  • size() is not something to use as a strict concurrent invariant.

Practice Tasks:

  1. Replace a shared HashMap counter with ConcurrentHashMap<String, LongAdder> and test it with 8 threads.
  2. Write a small program that shows put(null, ...) and put(..., null) both fail.
  3. Compare a synchronized map and ConcurrentHashMap under a thread pool, then note the throughput difference when many threads hit the same keys.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.LongAdder; public class ConcurrentHashMapInternalsDemo { public static void main(String[] args) throws InterruptedException { // A shared map for concurrent counting. // LongAdder is a good match for hot counters because many threads can increment it with less contention. ConcurrentHashMap<String, LongAdder> counts = new ConcurrentHashMap<>(); int threads = 6; int iterationsPerThread = 50_000; ExecutorService pool = Executors.newFixedThreadPool(threads); CountDownLatch start = new CountDownLatch(1); CountDownLatch done = new CountDownLatch(threads); for (int i = 0; i < threads; i++) { pool.execute(() -> { try { start.await(); for (int j = 0; j < iterationsPerThread; j++) { // computeIfAbsent is atomic for this key: one counter object gets installed and reused. counts.computeIfAbsent("java", k -> new LongAdder()).increment(); // A second key shows that the map can hold multiple independent hot spots. if ((j & 15_383) == 0) { counts.computeIfAbsent("concurrency", k -> new LongAdder()).increment(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { done.countDown(); } }); } start.countDown(); done.await(); pool.shutdown(); System.out.println("java count = " + counts.get("java").sum()); System.out.println("concurrency count = " + counts.get("concurrency").sum()); System.out.println("mappingCount = " + counts.mappingCount()); // Edge case: ConcurrentHashMap rejects nulls by design. // That is intentional because null is used as a special 'absent' marker in many concurrent algorithms. try { counts.put(null, new LongAdder()); } catch (NullPointerException ex) { System.out.println("null key rejected: " + ex.getClass().getSimpleName()); } try { counts.put("bad", null); } catch (NullPointerException ex) { System.out.println("null value rejected: " + ex.getClass().getSimpleName()); } // At this point the map is safe for shared access, but compound business rules still need care. // For example, checking one key and then updating another is not a single atomic transaction. } }