RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
TrickyJava#366 min readJul 11, 2026

Hashtable vs ConcurrentHashMap.

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

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.

🧠 Memory Map
Memory map — visual summary of this topic

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.

How Hashtable works under the hood

  1. Every core public method, such as get, put, and remove, is synchronized on the whole object.
  2. That means only one thread can be inside the map at a time, even if the threads are touching different keys.
  3. Buckets are handled with simple chaining, so collisions are stored in linked lists; bad collisions can make lookups slower.
  4. Because the lock is so coarse, a fast read can still wait behind a slow write.
  5. It also rejects null keys and values, which keeps old APIs simple but can surprise beginners.

How ConcurrentHashMap works under the hood

  1. Reads are usually non-blocking: the map uses visible state plus atomic updates so many threads can read at once.
  2. A put 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.
  3. If a bin already has entries, the thread locks only that bin, not the whole map.
  4. When many keys collide in one bin, Java 8 can treeify that bin into a red-black tree, which improves worst-case lookup from O(n) to roughly O(log n).
  5. Resizing is cooperative, so multiple threads can help move bins instead of one thread freezing the whole structure.

When to use which

  • Use ConcurrentHashMap for shared mutable maps in servers, caches, counters, session data, and other hot paths with many threads.
  • Use Hashtable only for old code or old APIs you cannot change.
  • If you truly need one big lock around the entire map, an explicit wrapper like Collections.synchronizedMap(new HashMap<>()) makes that choice clearer than reaching for Hashtable.
  • If your code is single-threaded, plain HashMap is usually simpler and faster.
AspectHashtableConcurrentHashMap
LockingOne global lockBin-level + CAS
ReadsBlocked by lockUsually non-blocking
NullsNo null key/valueNo null key/value
IterationLegacy, external syncWeakly consistent
DefaultsCapacity 11Capacity 16
Worst caseO(n)Tree bins reduce collisions

Performance and edge cases

  • Average lookup and insert are still about O(1) for both, but ConcurrentHashMap scales much better when many threads compete.
  • On a 16-core machine, one global lock can turn into a bottleneck fast; dozens of request threads may sit in 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.
  • Iterators on ConcurrentHashMap are weakly consistent: they do not throw ConcurrentModificationException and may reflect some updates that happen during iteration, but not necessarily all of them.
  • Java 8 changed the internals of 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.

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

  • Why is Hashtable considered legacy? It predates the modern Collections Framework and uses one global lock, so it scales poorly. It still exists for backward compatibility, not as the best default choice.
  • Is ConcurrentHashMap fully lock-free? No. Reads are usually lock-free, but updates may lock a bin when needed. The important improvement is that it avoids one lock for the whole map.
  • Can ConcurrentHashMap store nulls? No. Null would make it impossible to tell whether get returned “missing” or “present with null,” so the map forbids both null keys and null values.
  • Are ConcurrentHashMap iterators fail-fast? No. They are weakly consistent, which means they tolerate concurrent changes instead of throwing ConcurrentModificationException.
  • What if I need to increment a counter safely? Use merge, compute, or an AtomicInteger value. Do not do get then put unless you hold the right lock.
  • Tricky: If Hashtable is synchronized, is 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.
  • Tricky: Does CHM guarantee a perfectly stable view during iteration? No. It gives a usable live view, not a snapshot. You may see some concurrent updates, but you should copy the data first if you need a frozen view.
  • Tricky: Is Hashtable always slower than HashMap? Usually yes in concurrent code, but the real comparison is different: Hashtable is thread-safe with coarse locking, while HashMap is not thread-safe at all.

Common Mistakes:

  • Mistake: Saying Hashtable and ConcurrentHashMap are the same because both are thread-safe. Correction: Hashtable uses one global lock; ConcurrentHashMap scales with much finer-grained coordination.
  • Mistake: Thinking ConcurrentHashMap allows null keys or values. Correction: It rejects both, just like Hashtable.
  • Mistake: Using get then put for counters. Correction: Use merge, compute, or an atomic value type.
  • Mistake: Assuming iteration is snapshot-safe. Correction: 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.
  • Both reject null keys and null values.
  • ConcurrentHashMap supports atomic helpers like putIfAbsent, compute, and merge.
  • Average time for get/put is about O(1), but CHM handles contention much better and can treeify long collision bins.
  • If you need a consistent snapshot, copy the map first.

Practice Tasks:

  • Write a counter with ConcurrentHashMap<String, Integer> using merge.
  • Modify the example to use AtomicInteger as the value type and compare the style.
  • Remove the external synchronized block from the Hashtable update and observe why the count can become wrong.
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 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. } }