HashMap vs Hashtable — Hashtable Chokes Under Concurrency
Hashtable's internal locking caused 2s latency and 80% throughput drop in a payment service.
- HashMap: no synchronization, allows one null key and multiple null values, fast in single-threaded.
- Hashtable: synchronizes every method, disallows null keys/values, legacy since Java 1.0.
- ConcurrentHashMap: modern thread-safe alternative with lock stripping (CAS-based in Java 8+).
- Single-threaded: HashMap outperforms Hashtable by ~40% due to zero sync overhead.
- Multi-threaded: ConcurrentHashMap scales linearly; Hashtable serializes all access.
- Biggest mistake: using Hashtable in new code — reach for ConcurrentHashMap instead.
HashMap and Hashtable both store key-value pairs. The difference is that Hashtable is thread-safe by default (every method is synchronised) while HashMap is not. That sounds like Hashtable should be preferred — but full synchronisation per method is actually too coarse for most concurrent use cases, and it costs performance even in single-threaded code. Modern Java has ConcurrentHashMap, which provides real thread-safety with much better throughput.
Hashtable predates the Java Collections Framework (it's been around since Java 1.0). It's a legacy class. If you're writing new code and thinking about thread-safety in a Map, the answer is ConcurrentHashMap, not Hashtable. HashMap for single-threaded code, ConcurrentHashMap for concurrent code. Hashtable for legacy code you're maintaining.
Key Differences with Code Examples
HashMap and Hashtable share the same Map interface but differ fundamentally in thread-safety, null handling, and performance characteristics. HashMap is not synchronized — two threads calling put() simultaneously can corrupt internal data structures. Hashtable synchronizes every method, making it thread-safe but at a huge cost. The code below demonstrates the null behaviour and basic usage. For most modern concurrent needs, ConcurrentHashMap is the right answer because it uses lock-striping (or CAS in Java 8+) instead of coarse-grained synchronization.
But here's the thing: reading the docs isn't enough. In a real codebase, you'll find maps that look like they're used single-threaded but aren't — hidden callbacks, scheduled tasks, or web threads all touching the same map. Defaulting to Hashtable because "it's safe" costs you performance you'll never get back.
Performance and Synchronization Overhead
Hashtable uses intrinsic locks (synchronized) on every public method — get, put, containsKey, size, everything. That means even two threads doing read-only operations on different keys compete for the same lock. In contrast, ConcurrentHashMap (Java 8+) uses a combination of CAS operations and lock-striping per bin (aka segment-level locking in earlier versions). Reads are almost always lock-free, writes lock only the specific bin.
What does this mean in production? If you have 100 threads reading from a map concurrently, Hashtable serializes them, creating a queue. ConcurrentHashMap lets them all proceed with minimal contention. The throughput difference grows linearly with the number of threads.
Real benchmark: A 16-core server with a read-heavy workload (80% reads, 20% writes) — Hashtable delivers maybe 2000 ops/s. ConcurrentHashMap delivers over 50,000 ops/s. That's not a minor gain; it's the difference between a working system and a fire.
- Hashtable: one lock → all threads queue up.
- ConcurrentHashMap (Java 8+): CAS for bins + locks only on resize and for some write operations.
- Result: 20-thread load on Hashtable can be 10x slower than single-thread; ConcurrentHashMap scales near-linearly.
Collections.synchronizedMap() — but beware of iteration and compound operations.Null Handling: A Common Source of Bugs
HashMap allows one null key and any number of null values. Hashtable and ConcurrentHashMap throw NullPointerException for any null key or value. This difference seems small but causes cryptic production failures.
Imagine an application that receives data from an external source where fields can be null. If you store that data in a Hashtable and a null sneaks in — bam, NPE at an unpredictable point. With HashMap, the null is stored without error, which might hide a logic bug. The trade-off: HashMap lets you reason about missing data via get() returning null; ConcurrentHashMap forces you to be explicit about absent values using computeIfAbsent() or Optional.
Here's a pattern that often catches teams: you start with HashMap, everything works fine. Then you switch to ConcurrentHashMap for thread-safety, and suddenly your app crashes because something passes null. The JVM doesn't tell you where; you need to trace every put() call. That's why we recommend auditing all call sites with a simple grep before migration.
Iteration and Concurrent Modification
HashMap and Hashtable both use fail-fast iterators: if the map is structurally modified (add/remove) after the iterator is created, except through the iterator's own remove() method, it throws ConcurrentModificationException. This is a design choice to catch bugs quickly — but it's brutal in production if you're iterating and another thread modifies the map.
ConcurrentHashMap uses a weakly consistent iterator: it reflects the state at some point during iteration and may not reflect subsequent modifications. No ConcurrentModificationException is thrown. This makes ConcurrentHashMap safe for iteration even while other threads are writing, but you must accept that you might see stale entries.
Practical rule: never iterate over a HashMap or Hashtable that is concurrently modified — it will crash. For ConcurrentHashMap, iteration is safe but not deterministic.
One concrete example: a cache refresh thread iterates over a HashMap to evict expired entries while request threads are adding new entries. That's a guaranteed CME. Copying the entry set before iteration (new HashMap<>(map)) works but doubles memory momentarily. The better fix: use ConcurrentHashMap.
Migrating from Hashtable to ConcurrentHashMap
If you're maintaining legacy code with Hashtable, migrating to ConcurrentHashMap is straightforward but needs care. The two classes share the same interface (Map) so most code compiles unchanged.
- Nulls: if your code ever stores null keys or values, ConcurrentHashMap will throw NPE. You must add null checks or use Optional.
- Enumeration: Hashtable's
elements()andkeys()return Enumeration, not Iterator. ConcurrentHashMap's keySet().iterator() returns an Iterator. Code relying on Enumeration must be updated. - Size: Hashtable's
size()is accurate; ConcurrentHashMap'ssize()is an estimate under concurrent writes. If exact count is needed, use mappingCount() (returns long) or external synchronization. - Atomicity: replace compound operations (if (!map.containsKey(k)) map.put(k,v)) with ConcurrentHashMap's atomic methods: putIfAbsent(), computeIfAbsent(),
replace().
A pattern that catches teams: they change the variable type from Hashtable to ConcurrentHashMap but forget to update Enumeration loops. The code compiles because Hashtable also has elements()? No, ConcurrentHashMap does not have elements(). So you'll get a compile error. That's good. But if you have casts or reflection, it can break silently.
new Hashtable<>() with new ConcurrentHashMap<>() in constructors.
2. Change variable types from Hashtable to Map or ConcurrentHashMap.
3. Audit all put() calls for null keys/values – add null checks.
4. Replace compound get/put sequences with atomic methods.
5. Replace Enumeration usage with iterators.PaymentService Slows to a Crawl After 'Thread-Safe' Fix
get() and put() method, serializing all threads even for read-heavy workloads. In a high-concurrency payment service, this creates a single bottleneck.- Full method synchronization is rarely the right concurrency solution.
- Use ConcurrentHashMap for thread-safe maps in new code.
- Always benchmark before and after concurrency 'fixes'.
map.get()Hashtable.get() appears slow in flame graphsKey takeaways
Common mistakes to avoid
5 patternsUsing Hashtable for new multi-threaded code
Expecting Hashtable to be safe for compound operations
replace().Putting null keys into a map that might be iterated concurrently
Assuming Hashtable's iteration is safe under concurrent modification
Using ConcurrentHashMap.get() followed by put() instead of computeIfAbsent()
Interview Questions on This Topic
What is the difference between HashMap and Hashtable in Java?
Frequently Asked Questions
That's Collections. Mark it forged?
4 min read · try the examples if you haven't