HashMap vs Hashtable — Hashtable Chokes Under Concurrency
Hashtable's internal locking caused 2s latency and 80% throughput drop in a payment service.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- 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.
HashMap vs Hashtable — Hashtable Chokes Under Concurrency
HashMap and Hashtable are both hash-based map implementations in Java, but they differ fundamentally in thread-safety and performance. Hashtable synchronizes every method, making it thread-safe but serializing all access — a single lock per map. HashMap is unsynchronized, relying on the caller to manage concurrency externally. This one design choice cascades into dramatically different behavior under load.
In practice, Hashtable’s coarse locking means that even concurrent reads contend on the same monitor, turning a normally O(1) lookup into a bottleneck as thread count increases. HashMap, by contrast, allows true concurrent reads and, when paired with ConcurrentHashMap, offers lock-striping for writes. Hashtable also does not support null keys or values, while HashMap permits one null key and multiple null values — a subtle but frequent source of NullPointerExceptions when migrating code.
Use HashMap for single-threaded or externally synchronized contexts; it’s faster and more flexible. For concurrent access, never use Hashtable — reach for ConcurrentHashMap instead. In legacy systems, Hashtable often survives as a default choice, but its performance collapses under moderate contention, making it a common source of mysterious latency spikes in production.
Hashtable.get() monitor, with CPU idle despite high request volume.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.HashMap Resizing: The Silent OutOfMemoryError You Didn't See Coming
Most devs think HashMap is just a faster Hashtable. They're wrong. The real killer? Default initial capacity and load factor. HashMap starts at 16 buckets. Hit 75% fill rate, it doubles. Sounds fine until you store a million entries in production and watch the GC implode during resize. Hashtable doesn't resize as aggressively, but that's cold comfort when your JVM heap is thrashing from repeated rehash operations. The rule: always set initial capacity when you know your data size. If you expect 10,000 entries, don't let HashMap resize seven times. Use new HashMap<>(10000 / 0.75f + 1) to pre-allocate. This isn't premature optimization — it's preventing a midnight PagerDuty alert. I've seen a 250ms latency spike from a single HashMap resize in a high-throughput Kafka consumer. Pre-sizing erased it.
put().(expectedSize / 0.75f) + 1 to prevent cascading resize-related latency and GC pauses in production.HashMap.equals() Lies to You — Hashtable Doesn't
Here's a bug that made me rewrite a payment reconciliation service at 3 AM. HashMap's equals() compares key-value pairs, but it ignores the map's type. A HashMap and a Hashtable with identical entries will report as equal. Sounds helpful? It's a landmine. Hashtable's equals() checks the other object is also a Hashtable. If you're migrating legacy code and mixing both in a single code path, your conditional logic silently fails. The worst case? A ConcurrentHashMap and a Hashtable are never equal, but HashMap equals says 'yep, same'. I've seen this cause duplicate processing in batch jobs because a set of HashMaps deduplicated incorrectly. Rule: never rely on HashMap.equals() for cross-type comparisons. Use explicit field-by-field checks, or stick to one Map implementation across your service boundary. Your future self will thank you.
HashMap.equals() returns true for any Map implementation with matching entries. Hashtable.equals() and ConcurrentHashMap.equals() do not. Never use HashMap in a Set<Map> collection if you're mixing implementations — you'll get duplicates or missed matches.HashMap.equals() is not symmetric with other Map implementations — stick to one Map type per code path to avoid silent equality bugs in collections and conditionals.Why Your Team Should Ditch Hashtable Before It Dumps You
You don't get to pick legacy tech — you inherit it. And if you're still reaching for Hashtable in 2024, you're asking for thread-safety theater. Yes, Hashtable synchronizes every method. But that synchronization is a coarse, global lock — every read and write contends on the same monitor. Under even moderate concurrency, your throughput collapses.
ConcurrentHashMap doesn't just beat Hashtable — it humiliates it. It uses fine-grained locking and lock-free reads. You get real scalability, not the illusion of safety. The WHY is simple: Hashtable was designed when single-core machines ruled. Your modern 64-core server will laugh at it — then choke.
Production truth: Never use Hashtable for new code. Never. If you need a thread-safe map, reach for ConcurrentHashMap. If you need a plain map, use HashMap. Hashtable sits in the uncanny valley — neither simple nor fast. Throw it out.
HashMap and Hashtable Are Both Wrong for Caching — Here's the Fix
You're caching API responses, database queries, or user sessions. HashMap is not thread-safe — two threads writing will corrupt it. Hashtable is thread-safe but slow. Neither handles eviction. Your cache grows unbounded until your heap screams and your GC pauses hit seconds.
You need a map that does three things: thread-safety, bounded size, and automatic eviction. Caffeine does this. Guava's Cache does this. Even ConcurrentHashMap with ScheduledExecutorService can work. But HashMap? That's a memory leak waiting for a production page.
Stop reinventing the wheel. Use Caffeine — it's the fastest Java cache library. Define max size, expiration policy, and eviction listener. Your production app will thank you. Your on-call rotation will thank you. Your code review will stop being a dumpster fire.
Hierarchy: Why HashMap and Hashtable Inherit Different Rules
HashMap and Hashtable sit in different branches of the Java Collections Framework, and that hierarchy dictates everything about their contract. HashMap extends AbstractMap and implements Map, making it a full citizen of the modern collections API. Hashtable extends Dictionary — a legacy abstract class that predates the Collections Framework entirely — while also implementing Map, but as an afterthought. This ancestry means HashMap inherits fail-fast iterators, optional operations, and the null-friendly design of AbstractMap. Hashtable retains the older, synchronized, null-hostile behavior from Dictionary. When you write code that accepts a Map reference, passing a Hashtable works syntactically but forces callers into the broken synchronization and null restrictions of its lineage. Always program to the Map interface, and when you need thread safety, use ConcurrentHashMap — it inherits from the same modern tree as HashMap while adding proper concurrency.
elements() or keys() locks you into legacy iteration patterns. Switch to Map.entrySet() and ConcurrentHashMap to stay framework-compatible.When to Use Each: Speed vs Safety Tradeoff
HashMap wins on single-threaded speed — no synchronization overhead, resizing tuned for throughput, null keys allowed. Hashtable is strictly worse here: every put and get acquires a lock, bloating contention on multicore systems. But Hashtable’s synchronized methods do guarantee visibility under naive multithreading (at massive cost). The real choice isn’t HashMap vs. Hashtable — it’s HashMap vs. ConcurrentHashMap. For read-heavy workloads, use ConcurrentHashMap with the lock-striping strategy. For write-heavy or caching scenarios, neither works because Hashtable’s global lock serializes writes and HashMap’s non-thread-safe resizing corrupts the internal table. Rule of thumb: single-threaded → HashMap; multi-threaded reads with writes → ConcurrentHashMap; legacy interop → Hashtable only as a bridge to old code scheduled for replacement.
Overview: HashMap vs Hashtable
HashMap and Hashtable are both hash-based implementations of the Map interface in Java, but they serve different eras and philosophies. Hashtable, introduced in Java 1.0, is a legacy class that synchronizes every method — making it thread-safe but inherently slow. HashMap arrived with Java 1.2 as part of the Collections Framework, designed for single-threaded performance. The core distinction? Synchronization. Hashtable uses internal locks on all operations, while HashMap offers no thread safety. This choice impacts iteration behavior, null handling, and scalability. Modern Java developers should prefer HashMap for most use cases, reserving Hashtable only for legacy interop or when you absolutely need a synchronized map without customization — though even then, ConcurrentHashMap is superior. Understanding this tradeoff is the foundation for all other differences in performance, null handling, and iteration semantics.
Conclusion: HashMap or Hashtable — Choose Wisely
After analyzing performance, null handling, iteration, and concurrency, one truth emerges: Hashtable is a relic. HashMap should be your default for single-threaded or externally synchronized scenarios. If thread safety is required, skip Hashtable entirely and use ConcurrentHashMap — it provides better scalability with fine-grained locking, better null handling, and is fully compatible with modern Java idioms. For caching, neither HashMap nor Hashtable is ideal due to resizing overhead and concurrency issues — consider Caffeine or Guava caches instead. The migration from Hashtable to ConcurrentHashMap is straightforward and pays dividends in performance and correctness. Remember: HashMap allows one null key and multiple null values; Hashtable forbids all nulls — choose based on whether null is valid in your domain. Finally, never rely on iteration order from either; use LinkedHashMap or TreeMap if ordering matters. The Java landscape has evolved — let legacy maps retire gracefully.
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 graphsjstack <pid> | grep -A 10 'java.util.Hashtable'jcmd <pid> Thread.printKey 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
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Collections. Mark it forged?
10 min read · try the examples if you haven't