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.”
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.
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.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.
null keys or values: null is reserved to mean “absent,” which keeps reads simple and avoids ambiguity during races.size() can be expensive or only approximate in effect. For large concurrent counters, prefer mappingCount() or design around approximate metrics.putIfAbsent or computeIfAbsent is atomic for that key, but a multi-step business rule across several keys still needs extra synchronization.concurrencyLevel is only a sizing hint now; it does not create segments anymore.| Type | Locking | Nulls | Iteration | Typical use |
|---|---|---|---|---|
ConcurrentHashMap | Bin-level | No | Weakly consistent | Shared concurrent state |
Hashtable | Whole map | No | Fail-fast-ish legacy behavior | Old code, rarely ideal |
synchronizedMap | Whole map via wrapper | Depends on backing map | Must synchronize manually | Simple low-contention cases |
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.
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.
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:
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.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.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.ConcurrentModificationException just because another thread updates the map.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.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:
null to mean missing data: This map forbids null, so missing must be represented with a different value or an explicit check.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:
null keys or values.size() is not something to use as a strict concurrent invariant.Practice Tasks:
HashMap counter with ConcurrentHashMap<String, LongAdder> and test it with 8 threads.put(null, ...) and put(..., null) both fail.ConcurrentHashMap under a thread pool, then note the throughput difference when many threads hit the same keys.