Hashtable is one of the original collection classes in Java, around since version 1.0, long before the Collections Framework was added in Java 1.2. It's a synchronized map: every method is guarded by a lock, so two threads can't be inside the same Hashtable at the same time. It still works, still shows up in old code, and shows up in interviews as the "what's the difference between this and HashMap?" question. This lesson covers what Hashtable is, how its synchronization actually works, why every method being synchronized turns out to be a problem, and what to use instead.
Hashtable predates the Collections Framework. The same thing happened with Vector. Both classes were retrofitted later to implement the modern interfaces (Hashtable now implements Map), but their original design choices came from a time when the Java team assumed a shared object should be thread-safe by default.
The class lives in java.util and stores key-value pairs using a hash table internally, like HashMap. The shape of the API is almost identical:
Swapping Hashtable for HashMap in that program produces the same output. The shape of the API is the same. What's different is what happens underneath each put and get call, and what restrictions the class places on the keys and values passed in.
The defaults:
| Property | Hashtable | HashMap |
|---|---|---|
| Initial capacity | 11 | 16 |
| Load factor | 0.75 | 0.75 |
| Null keys | Not allowed | One allowed |
| Null values | Not allowed | Allowed |
| Synchronization | Every method | None |
| Introduced in | Java 1.0 | Java 1.2 |
| Iterator behavior | Fail-fast (modern), originally Enumeration | Fail-fast |
The initial capacity of 11 is a holdover from the original design, which preferred prime numbers for the internal array size to spread hash codes evenly. HashMap later switched to powers of two because it allows faster modulo-by-mask arithmetic. The 0.75 load factor matches because both classes resize when 75% of the buckets are occupied.
Hashtable rejects null as either a key or a value. This is the first behavior difference that tends to surface when porting code from HashMap to Hashtable or the other way around.
Both put calls throw NullPointerException immediately. The original Hashtable design chose this behavior so that get(key) returning null could only mean one thing: the key isn't in the map. With HashMap, a null return from get is ambiguous, since it might mean the key is missing or it might mean the value stored is null. Telling them apart requires containsKey.
The trade-off was clear when Hashtable was the only map available. Today, the null restriction is more of an irritant than a feature, because most code that runs on a HashMap will break the moment a Hashtable is dropped in and fed a null value.
Every public method on Hashtable is declared synchronized. That keyword on a non-static method means the method acquires the lock on the Hashtable instance before its body runs, and releases it when the body returns. While one thread holds the lock, no other thread can enter any synchronized method on the same instance.
A simplified put inside Hashtable:
Every method follows the same pattern: get, put, remove, size, containsKey, isEmpty, and so on. They all acquire the same lock on the same Hashtable instance.
This guarantees single-thread atomicity for individual operations. If thread A is inside put, thread B calling get on the same Hashtable waits until A is done. The internal hash array can't be observed in a half-modified state.
A timeline shows the serialization:
The diagram shows two threads competing for the lock. Thread A wins the race, runs its put, and releases the lock. Thread B, which arrived at almost the same time, waits the whole duration of A's put before it can start its get. Even though get doesn't modify anything, it still has to wait, because the lock guards every method without distinguishing reads from writes.
A small program uses Hashtable from two threads and works correctly only because of that lock:
The exact intermediate values depend on thread scheduling, so the reader's lines might be different from run to run. None of the reads ever sees a corrupted state, and the final value reflects the last successful put. With HashMap instead, repeated runs under contention could occasionally land in an infinite loop inside HashMap.get, due to a known race condition that existed up through Java 7. Hashtable doesn't have that problem, because the lock prevents two threads from being inside put at the same time.
Per-method synchronization is fine in theory and weak in practice for two separate reasons. The first is performance under contention. The second is that it doesn't make compound operations safe.
The performance issue is the obvious one. Every read, including ones that don't change anything, takes the lock. With ten threads all calling get on a Hashtable, nine of them are blocked while the tenth runs. The hash table itself can support concurrent reads, but the lock forces them into a queue. As thread count goes up, throughput on Hashtable flattens and then drops, because most threads are sitting in the wait queue rather than doing work.
The compound-operation issue is more subtle. Consider an inventory module that wants to decrement stock for a product, but only if the product is in the table:
Output (typical):
That looks fine. The problem only shows up when both threads run truly in parallel. The bug is that the lock is released between steps (1), (2), and (3). Two threads can both pass the containsKey check, both read 10, and both write 9. One decrement is lost. The final stock is 9 instead of 8.
The lock makes each individual call atomic. It does nothing for a sequence of calls. Wrapping the whole sequence in a synchronized block on the same lock is required:
That works because Hashtable.put, Hashtable.get, and Hashtable.containsKey all lock on the Hashtable instance itself, so an external synchronized (stock) block uses the same lock. But now the code is as locked as before, and the per-method synchronization is doing nothing extra. The cost is paid but no benefit beyond manual locking is gained.
Every Hashtable operation acquires and releases the instance lock. Under heavy multi-threaded read load, throughput is bounded by lock contention rather than by the hash table's actual capacity. For new code that needs concurrent access, ConcurrentHashMap allows multiple readers and many writers to run in parallel.
Since Java 1.2, Hashtable has had two replacements that cover different needs. Neither is the same thing as Hashtable, but together they cover what Hashtable was trying to do.
Collections.synchronizedMap wraps any Map (typically a HashMap) in a synchronized adapter. The adapter holds a single lock and acquires it inside every method, just like Hashtable. The big difference is that any underlying map (HashMap, LinkedHashMap, TreeMap) can be wrapped, and the wrapper adapts it for thread safety.
The underlying HashMap accepts one null key and any number of null values, and the synchronized wrapper passes those through. This is closer to "modern semantics with Hashtable-style synchronization."
The compound-operation problem is the same. synchronizedMap locks each method, not each sequence. Multi-step logic still has to be wrapped in an external synchronized block, using the wrapper itself as the lock object. Iteration is a particular issue: hold the lock manually while iterating, or risk ConcurrentModificationException if another thread modifies the map mid-iteration.
ConcurrentHashMap arrived in Java 5 and is the modern answer for concurrent maps. It uses fine-grained locking and lock-free reads, so multiple threads can read and write in parallel without serializing on a single lock. For new code that needs a thread-safe map, ConcurrentHashMap is the first choice.
| Use case | Pick |
|---|---|
| Single-threaded code | HashMap |
Legacy code that already uses Hashtable | Leave it, unless you measure a problem |
| New thread-safe map, simple needs | ConcurrentHashMap |
| Wrap an existing non-thread-safe map for occasional concurrent access | Collections.synchronizedMap |
| Heavy read concurrency | ConcurrentHashMap |
Hashtable is effectively obsolete for new code, but it hasn't gone anywhere. It still appears in three places:
The first is legacy application code, especially codebases that started in the late 1990s. An older inventory module written in Java 1.2 will use Hashtable for any shared map, because that was the default choice at the time. Replacing it is usually safe but requires testing, since the null rules are different from HashMap.
The second is legacy APIs. A few classes in the JDK and in older libraries return or accept Hashtable instances directly. The most visible example is java.util.Properties, which extends Hashtable<Object, Object>. Loading a .properties file uses a Hashtable underneath. The System.getProperties() method also returns a Properties instance.
The output confirms that Properties is built on Hashtable. The underlying Hashtable API on a Properties instance shouldn't be used directly, but the inheritance is there.
The third is JNDI (javax.naming.InitialContext) and a few enterprise APIs that take a Hashtable<String, String> for environment configuration. The signature is fixed by the API, so even modern code has to pass a Hashtable when calling those entry points.
For everything else, Hashtable is a teaching artifact. Know what it is, know why it's deprecated by convention (it's not deprecated by annotation), and know which replacement to use.
A small program that runs the same sequence on a Hashtable and a HashMap shows the behavior differences:
The same put call behaves differently depending on which map is on the left. The exceptions from Hashtable are the most common reason a port between HashMap and Hashtable fails in tests but passes a casual read.
A second program shows the synchronization difference. Two threads bump a shared counter using both maps, with no external locking:
Output (typical):
Both numbers are below 2000, which would be the result if every increment had completed without interference. The lost-update problem is the same on both, because the get-then-put pair isn't atomic on either one. Hashtable protects each individual call, but not the pair. HashMap doesn't even protect the individual call, so the actual numbers vary more from run to run, and on rare occasions a HashMap race in older JVMs could leave the map in a state that crashes a subsequent get. Modern HashMap won't infinite-loop the way Java 7's version could, but the count is still wrong.
Hashtable's lock helps with a narrow set of failures (no corrupted internal state during a single call) but does nothing for compound operations. For concurrent work, ConcurrentHashMap is the appropriate tool, and it also exposes atomic helpers like compute, merge, and putIfAbsent that fix the lost-update problem cleanly.
10 quizzes