Interviewers love this topic because it reveals whether you understand the difference between thread-safe and scalable thread-safe.
Question: How do Hashtable and ConcurrentHashMap differ in locking, iteration, and null handling?
Answer: Hashtable is a legacy synchronized map that locks the whole table for many operations, so only one thread can make progress at a time. ConcurrentHashMap is built for multi-threaded code: reads are mostly lock-free, writes lock only a small part of the map, and iterators are weakly consistent. Both reject null keys and values.
Interview-Ready Answer: I would choose ConcurrentHashMap for almost all concurrent code because it scales much better under contention. Hashtable synchronizes the whole map, so even unrelated operations block each other. In Java 8+, ConcurrentHashMap uses finer-grained locking and CAS-based reads, which makes it a better fit for hot paths like counters, caches, and shared registries. I also remember that both maps reject null keys and values, so if I need nulls I’d use something else.
Hashtable is an older synchronized map. The key idea is simple: a monitor is the built-in lock attached to a Java object, and Hashtable uses that lock for many public methods. That makes it safe, but also creates a bottleneck. ConcurrentHashMap was designed to reduce that bottleneck by letting unrelated operations happen at the same time.
put or get and grabs the table’s single monitor. While it holds that lock, other threads trying to read or write the map are blocked. The map computes the bucket, scans the linked list in that bucket, updates or returns the value, and then releases the lock.ConcurrentHashMap can treeify the bin into a red-black tree, which improves worst-case lookup from O(n) to O(log n) for that bin.ConcurrentHashMap coordinates resizing so multiple threads can help transfer bins instead of freezing the whole structure.if (map.get(k) == null) map.put(k, v) are not atomic just because the map is synchronized. That is the classic trap. For atomic read-modify-write logic, ConcurrentHashMap gives you putIfAbsent, compute, and merge.| Feature | Hashtable | ConcurrentHashMap |
|---|---|---|
| Locking | One big lock | Fine-grained |
| Reads | Blocked by lock | Mostly lock-free |
| Writes | Serialized | Per-bin / CAS |
| Nulls | No null key/value | No null key/value |
| Iteration | Legacy, synchronized views | Weakly consistent |
| Throughput | Poor under contention | Much better |
ConcurrentHashMap for shared caches, counters, session maps, registries, and anything with many threads.Hashtable only for legacy code you cannot change easily.Collections.synchronizedMap(...) is often clearer than introducing Hashtable, but it still uses coarse locking.O(1) for both, but heavy collisions can hurt either structure.ConcurrentHashMap no longer relies on fixed segments; Java 7 used segments more heavily, which is why older explanations often talk about “segment locking.”Hashtable defaults to a small initial capacity of 11 and a load factor of 0.75, which is a reminder that it is a very old design.size() and iteration in a concurrent map can be less predictable while updates are happening.ConcurrentHashMap does not mean “no locking at all”; it means “less blocking and less contention.”Real-World Example: Imagine an e-commerce checkout service tracking inventory counts for thousands of SKUs. Each request thread reads stock, reserves items, and updates counters. If the team uses Hashtable for the inventory map, every read/write fights for the same global lock, so traffic spikes turn into blocked threads and slow checkouts. The system may still be “thread-safe,” but the p95 latency climbs because all threads wait in line.
A common outage looks like this: order placement starts timing out during a sale, thread dumps show many threads blocked on the same monitor, and logs show retries or slow reservation calls. If the team also did a read-modify-write update like get then put without extra synchronization, stock counts can be wrong and customers may see “item available” followed by checkout failure. With ConcurrentHashMap plus merge or compute, the inventory update becomes atomic per key and much more scalable.
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 HashtableVsConcurrentHashMapDemo {
// Hashtable synchronizes individual methods, but a read-modify-write update
// is still a compound action. We add our own lock so the increment is correct.
private static void incrementHashtable(Hashtable<String, Integer> table, String key) {
synchronized (table) {
Integer current = table.get(key);
table.put(key, current == null ? 1 : current + 1);
}
}
// ConcurrentHashMap gives us an atomic per-key update API.
// merge() avoids the classic get-then-put race.
private static void incrementConcurrent(ConcurrentHashMap<String, Integer> map, String key) {
map.merge(key, 1, Integer::sum);
}
public static void main(String[] args) throws InterruptedException {
Hashtable<String, Integer> hashtable = new Hashtable<>();
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
int threads = 8;
int iterationsPerThread = 10_000;
int expected = threads * iterationsPerThread;
ExecutorService pool = Executors.newFixedThreadPool(threads);
CountDownLatch done = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
try {
for (int j = 0; j < iterationsPerThread; j++) {
incrementHashtable(hashtable, "sku-42");
incrementConcurrent(concurrentMap, "sku-42");
}
} finally {
done.countDown();
}
});
}
done.await();
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("Expected count: " + expected);
System.out.println("Hashtable count: " + hashtable.get("sku-42"));
System.out.println("ConcurrentHashMap count: " + concurrentMap.get("sku-42"));
// Edge case: both maps reject null keys/values.
try {
hashtable.put(null, 123);
} catch (NullPointerException e) {
System.out.println("Hashtable rejects null key: " + e.getClass().getSimpleName());
}
try {
concurrentMap.put("null-value-demo", null);
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap rejects null value: " + e.getClass().getSimpleName());
}
// If you remove the synchronized block above from Hashtable increment logic,
// the get-then-put sequence becomes a race and the count can be wrong.
}
}Follow-up & Tricky Questions:
null would make get(key) ambiguous: it would be impossible to tell whether a key is absent or mapped to null. That design keeps concurrent reads simple and safe.get then put without an outer lock, because the sequence itself is not atomic.Common Mistakes:
Memory Hook: Think of Hashtable as one cashier for the whole store and ConcurrentHashMap as many cashiers with separate lanes. Both are safe, but only one scales when the line gets long.
Cheat Sheet:
Hashtable = legacy, coarse-grained locking, poor contention behavior.ConcurrentHashMap = modern, high-throughput, fine-grained concurrency.null keys and values.ConcurrentHashMap iterators are weakly consistent, not fail-fast.merge, compute, or putIfAbsent.ConcurrentHashMap almost every time.Practice Tasks:
ConcurrentHashMap.merge.Hashtable and add the correct external synchronization.