RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
MediumJava#1017 min readJul 11, 2026

How do Hashtable and ConcurrentHashMap differ in locking, iteration, and null handling?

practice
learning
collections
concurrency
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

Big picture

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.

How they work under the hood

  1. Hashtable: a thread enters 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.
  2. ConcurrentHashMap: a read usually does not block. It looks up the bucket with volatile-style visibility guarantees, then walks the node chain. A write first tries fast paths such as CAS (compare-and-swap, an atomic CPU-assisted update) when the bin is empty. If the bin already contains nodes, it locks only that bin, not the whole map.
  3. Collision handling: if many keys land in the same bucket, Java 8+ 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.
  4. Resize: both maps grow when the load factor threshold is exceeded, but ConcurrentHashMap coordinates resizing so multiple threads can help transfer bins instead of freezing the whole structure.
  5. Compound actions: operations like 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.

Comparison table

FeatureHashtableConcurrentHashMap
LockingOne big lockFine-grained
ReadsBlocked by lockMostly lock-free
WritesSerializedPer-bin / CAS
NullsNo null key/valueNo null key/value
IterationLegacy, synchronized viewsWeakly consistent
ThroughputPoor under contentionMuch better

When and why to use each

  • Use ConcurrentHashMap for shared caches, counters, session maps, registries, and anything with many threads.
  • Use Hashtable only for legacy code you cannot change easily.
  • If you just need a synchronized map for older code, Collections.synchronizedMap(...) is often clearer than introducing Hashtable, but it still uses coarse locking.

Complexity and version notes

  • Average lookup and update are O(1) for both, but heavy collisions can hurt either structure.
  • In Java 8+, 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.

Important edge cases

  • 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.”
  • A synchronized map does not automatically make your whole algorithm thread-safe if your logic uses multiple map calls.

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.

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

  • Why does ConcurrentHashMap reject nulls? Because 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.
  • Is Hashtable ever a good choice today? Only for legacy APIs or old codebases where changing the type is risky. For new code, it is usually a smell because its coarse locking hurts throughput.
  • What changed in ConcurrentHashMap in Java 8? The implementation moved away from fixed segments and toward per-bin locking plus CAS-based fast paths. That made it simpler and often faster under contention.
  • Is Collections.synchronizedMap the same as Hashtable? Not exactly, but both use coarse locking around a backing map. It can be a reasonable bridge for old code, but it still does not scale like ConcurrentHashMap.
  • When would you use computeIfAbsent or merge? When the update depends on the existing value, such as counters, caches, or per-key initialization. These methods help you write atomic logic without manual locking.
  • Does ConcurrentHashMap guarantee no blocking? No. It reduces blocking a lot, but a write may still lock a bin, and resizing can involve coordination among threads.
  • Can Hashtable fail under concurrency even though it is synchronized? Yes, if your algorithm does multiple calls like get then put without an outer lock, because the sequence itself is not atomic.
  • Does ConcurrentHashMap iterator throw ConcurrentModificationException? No. Its iterators are weakly consistent, so they can reflect some updates during iteration without failing.
  • Tricky: Is ConcurrentHashMap completely lock-free? No. Reads are mostly lock-free, but writes can lock a bin when needed, so the correct description is “highly concurrent,” not “lock-free everywhere.”
  • Tricky: If Hashtable is synchronized, do I still need external synchronization? For single method calls, usually no. For compound operations like “check then act” or “read then modify,” yes, you still need external synchronization or a better atomic API.

Common Mistakes:

  • Mistake: “Hashtable is thread-safe, so it is always fine.” Correction: It is thread-safe at the method level, but that coarse lock often becomes a performance problem.
  • Mistake: “ConcurrentHashMap never locks.” Correction: It reduces locking dramatically, but writes can still lock a bin.
  • Mistake: “Synchronized map means all compound logic is safe.” Correction: Only the single call is protected; multi-step logic still needs atomic APIs or external locking.
  • Mistake: Forgetting null behavior. Correction: Both maps reject null keys and values, which is a common interview trap because HashMap does allow them.

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.
  • Both reject null keys and values.
  • ConcurrentHashMap iterators are weakly consistent, not fail-fast.
  • For atomic updates, prefer merge, compute, or putIfAbsent.
  • For new concurrent code, choose ConcurrentHashMap almost every time.

Practice Tasks:

  • Write a word-count program using ConcurrentHashMap.merge.
  • Replace that logic with Hashtable and add the correct external synchronization.
  • Run both under a thread pool and compare correctness and throughput.
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.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. } }