Hook: Interviewers love this one because it looks like a simple map comparison, but it really tests locking, iteration, and atomic updates.
Question: Hashtable vs ConcurrentHashMap.
Answer: Hashtable is the older synchronized map: every public method grabs one big lock, so even reads can block. ConcurrentHashMap is built for concurrent access, so it avoids one global lock and gives much better throughput under load. Both reject null keys and null values, but ConcurrentHashMap also gives safer concurrent iteration and atomic helpers like merge and putIfAbsent.
Interview-Ready Answer: I’d say Hashtable is the legacy synchronized map, while ConcurrentHashMap is the modern concurrent choice. Hashtable locks the whole map for each method, so threads contend even on reads; ConcurrentHashMap uses much finer-grained synchronization and lock-free reads, so it scales far better. Both disallow null keys and values, but for new code I would almost always choose ConcurrentHashMap, especially when many threads are reading and updating the map at the same time.
Detailed Explanation: Think of Hashtable as one cashier for the whole store, while ConcurrentHashMap is many cashiers working different aisles. Both are maps, both store key/value pairs, but they protect access in very different ways.
Hashtable works under the hoodget, put, and remove, is synchronized on the whole object.null keys and values, which keeps old APIs simple but can surprise beginners.ConcurrentHashMap works under the hoodput on an empty bin can use CAS, which means compare-and-swap: an atomic CPU operation that updates only if the current value matches what the thread expected.O(n) to roughly O(log n).ConcurrentHashMap for shared mutable maps in servers, caches, counters, session data, and other hot paths with many threads.Hashtable only for old code or old APIs you cannot change.Collections.synchronizedMap(new HashMap<>()) makes that choice clearer than reaching for Hashtable.HashMap is usually simpler and faster.| Aspect | Hashtable | ConcurrentHashMap |
|---|---|---|
| Locking | One global lock | Bin-level + CAS |
| Reads | Blocked by lock | Usually non-blocking |
| Nulls | No null key/value | No null key/value |
| Iteration | Legacy, external sync | Weakly consistent |
| Defaults | Capacity 11 | Capacity 16 |
| Worst case | O(n) | Tree bins reduce collisions |
O(1) for both, but ConcurrentHashMap scales much better when many threads compete.BLOCKED state behind a single monitor.ConcurrentHashMap does not make compound actions magically safe. A get followed by a put is still two steps unless you use merge, compute, or putIfAbsent.ConcurrentHashMap are weakly consistent: they do not throw ConcurrentModificationException and may reflect some updates that happen during iteration, but not necessarily all of them.ConcurrentHashMap: it no longer uses the old segment design from Java 7; the concurrencyLevel constructor argument is mainly a sizing hint now.Real-World Story: Imagine a flash-sale checkout service that tracks remaining inventory per SKU. Early on, the team used Hashtable, and every stock update had to wait for the same global lock. Under load, request latency spiked, threads piled up in BLOCKED state, and the service started timing out even though the CPUs were not fully busy.
Later, someone switched to ConcurrentHashMap but kept the naive pattern get(key) then put(key, value - 1). That looked fine in testing, but in production two threads could read the same old count and overwrite each other. The result was overselling: logs showed inventory counts that did not match orders, support tickets mentioned customers buying the last item twice, and dashboards showed a growing gap between reserved and sold stock. The fix was to use atomic map operations like merge or compute, which update one key safely without external locking.
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main {
// Hashtable synchronizes each method, but a read-modify-write update is still a compound action.
// That means we must synchronize externally if we want "get current count, add 1, write back" to be atomic.
private static void incrementHashtable(Hashtable<String, Integer> table, String key) {
synchronized (table) {
table.put(key, table.getOrDefault(key, 0) + 1);
}
}
// ConcurrentHashMap gives atomic helpers for per-key updates, so merge is the right tool here.
private static void incrementConcurrentMap(ConcurrentHashMap<String, Integer> map, String key) {
map.merge(key, 1, Integer::sum);
}
public static void main(String[] args) throws InterruptedException {
Hashtable<String, Integer> table = new Hashtable<>();
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
// Edge case: both maps reject null keys and null values.
// This is intentional: null would make concurrent reads ambiguous.
try {
table.put(null, 1);
} catch (NullPointerException e) {
System.out.println("Hashtable rejects null key: " + e.getClass().getSimpleName());
}
try {
table.put("x", null);
} catch (NullPointerException e) {
System.out.println("Hashtable rejects null value: " + e.getClass().getSimpleName());
}
try {
chm.put(null, 1);
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap rejects null key: " + e.getClass().getSimpleName());
}
try {
chm.put("x", null);
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap rejects null value: " + e.getClass().getSimpleName());
}
int threads = 8;
int iterations = 10_000;
ExecutorService pool = Executors.newFixedThreadPool(8);
CountDownLatch done = new CountDownLatch(threads * 2);
for (int i = 0; i < threads; i++) {
pool.execute(() -> {
for (int j = 0; j < iterations; j++) {
incrementHashtable(table, "hits");
}
done.countDown();
});
pool.execute(() -> {
for (int j = 0; j < iterations; j++) {
incrementConcurrentMap(chm, "hits");
}
done.countDown();
});
}
done.await();
pool.shutdown();
pool.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("Hashtable hits = " + table.get("hits"));
System.out.println("ConcurrentHashMap hits = " + chm.get("hits"));
System.out.println("Expected hits = " + (threads * iterations));
// If you remove the synchronized block from incrementHashtable, the final count may be wrong,
// because the compound update is no longer atomic.
}
}Follow-up & Tricky Questions:
get returned “missing” or “present with null,” so the map forbids both null keys and null values.ConcurrentModificationException.merge, compute, or an AtomicInteger value. Do not do get then put unless you hold the right lock.get + put automatically safe? No. Each method is synchronized separately, but the pair of operations is not one atomic action unless you add your own external synchronization.Hashtable is thread-safe with coarse locking, while HashMap is not thread-safe at all.Common Mistakes:
Hashtable and ConcurrentHashMap are the same because both are thread-safe. Correction: Hashtable uses one global lock; ConcurrentHashMap scales with much finer-grained coordination.ConcurrentHashMap allows null keys or values. Correction: It rejects both, just like Hashtable.get then put for counters. Correction: Use merge, compute, or an atomic value type.ConcurrentHashMap iterators are weakly consistent, and Hashtable still needs external synchronization if other threads may modify it.Memory Hook: Hashtable is one locked door; ConcurrentHashMap is a building with many smaller doors. One person at a time vs many people moving in parallel.
Cheat Sheet:
Hashtable = legacy, synchronized on the whole map.ConcurrentHashMap = modern, concurrent, far better throughput.ConcurrentHashMap supports atomic helpers like putIfAbsent, compute, and merge.O(1), but CHM handles contention much better and can treeify long collision bins.Practice Tasks:
ConcurrentHashMap<String, Integer> using merge.AtomicInteger as the value type and compare the style.synchronized block from the Hashtable update and observe why the count can become wrong.