HashMap vs Hashtable vs ConcurrentHashMap in Java: A Beginner’s Guide
-
Last Updated: August 8, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
HashMap vs Hashtable vs ConcurrentHashMap in Java explained simply. Learn thread safety, null rules, locking, performance, and which Java map to pick and when.
Sooner or later, every Java developer runs into the HashMap vs Hashtable vs ConcurrentHashMap question. It shows up in code reviews. It shows up in interviews. And it shows up at 2 a.m. when a map quietly breaks under load.
On the surface, these three classes look like triplets. Each one stores key-value pairs. Each one gives you put and get. Swap one for another and your code still compiles.
Under the hood, though, they are not the same at all. One of them is fast but unsafe with threads. Another is safe, yet slow and dated. The third is safe and quick, although it plays by its own rules.
This article walks through all three in plain language. We will start with what each class is. Then we will look at how they handle nulls, locking, and speed.
Here is what we will cover:
You only need basic HashMap knowledge to follow this. No threading background required.
Before we compare, let us agree on the basics. A Map stores data as pairs. One part is the key, and the other part is the value.
Think of a phone contact list. The name is the key. The number is the value. Give it a name, and you get the number back right away.
A Map keeps each key only once. Put the same key twice, and the second value replaces the first one. Values have no such rule, so two keys can share a value.
That rule holds for all three classes we discuss today. HashMap, Hashtable, and ConcurrentHashMap all follow it.
Here is the short version before we dig in:
All three sit in the java.util package family. Hashtable came first, back in Java 1.0. HashMap arrived in Java 1.2, and ConcurrentHashMap landed in Java 5.
Most Java code uses HashMap. It is quick, it is simple, and it does not get in your way. For single-threaded work, it is almost always the right pick.
A HashMap keeps an internal array of slots. We call these slots buckets. Your key decides which bucket holds the entry.
The steps are simple. First, the map calls hashCode() on your key. Next, it turns that number into a bucket index. Finally, it drops the entry into that bucket.
Lookups run the same path in reverse. Hash the key, find the bucket, then check the entries there with equals(). Because it jumps straight to one bucket, a get() call is usually O(1).
Two different keys can land in the same bucket. We call that a collision, and it is normal. The map does not panic.
Entries in the same bucket form a small linked list. A lookup walks that short chain and compares keys one by one. With good hash codes, the chain stays tiny.
Java 8 added a nice touch here. When one bucket grows past eight entries, and the table itself holds at least 64 slots, that chain turns into a balanced tree. Worst-case lookup then drops from O(n) to O(log n).
HashMap allows one null key. It also allows any number of null values. That flexibility is handy in real code.
Map<String, String> map = new HashMap<>();
map.put(null, "no name"); // null key is fine
map.put("phone", null); // null value is fine
map.put("email", null); // another null value is fine
System.out.println(map.get(null)); // no name
System.out.println(map.size()); // 3The null key gets a hash of 0, so it always sits in bucket zero. Nothing special happens beyond that.
Here comes the catch. A HashMap has no locking at all. Two threads can walk into the same bucket at the same moment.
When that happens, things go wrong in quiet ways:
That last one used to cause an infinite loop in Java 7 and earlier. Java 8 changed the resize logic, so the endless loop is gone. Data loss, though, is still very much possible.
| INTERVIEW INSIGHT Interviewers love this follow-up: “Is HashMap safe if all threads only read?” Yes, it is. A HashMap that nobody writes to after publication is safe to share. Trouble starts the moment one thread modifies it. |
Words are one thing. Watching a map lose data is another. Run this snippet a few times and check the count.
Map<Integer, Integer> map = new HashMap<>();
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
map.put(i, i);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
// Expected 10000. You will often see less.
System.out.println(map.size());Both threads write the same 10,000 keys, so 10,000 entries should survive. Very often you get a smaller number. Sometimes you get a bigger one, which is even stranger.
Notice that nothing crashes. No exception, no warning, no red text in the console. The map just quietly holds the wrong data, and your bug surfaces days later.
There is one more piece worth knowing, because it explains when things break. A HashMap does not keep the same number of buckets forever.
A fresh HashMap starts with sixteen buckets. It also carries a load factor, which defaults to 0.75. Multiply those two and you get the threshold, which is twelve.
Add a thirteenth entry, and the map resizes. It builds a new table with double the buckets, then moves every entry across. Each key gets a fresh bucket index in the bigger table.
Why 0.75? Because it balances two costs. A lower number wastes memory on empty slots. A higher number packs buckets tightly, so chains grow and lookups slow down.
Now think about that resize with two threads running. One thread is halfway through moving entries. The other thread walks into the table and reads garbage. That single moment causes most HashMap horror stories.
Knowing the size in advance helps a lot. Pass it to the constructor, like new HashMap<>(1000), and you skip several resize rounds.
Hashtable shipped with Java 1.0, long before the Collections Framework existed. It was retrofitted later to implement Map. That history explains most of its quirks.
Every public method on Hashtable is synchronized. Calling get, put, remove, or size takes a lock on the whole object.
So the map is thread safe. Only one thread touches it at a time. Everyone else waits in line.
That design is easy to reason about, and it does work. But it is also the reason Hashtable struggles under load.
Picture a busy office with one door. Only one person may pass through at a time. Ten people wanting to enter form a queue, even though the office has plenty of room inside.
Hashtable works the same way. Ten threads reading ten different keys still line up. None of them touch the same data, yet they block each other anyway.
Add more threads and the queue only grows. This is why Hashtable does not scale on modern machines with many cores.
Hashtable rejects null keys and null values. Try either one and you get a NullPointerException at runtime.
Hashtable<String, String> table = new Hashtable<>();
table.put("city", "Pune"); // fine
table.put(null, "value"); // throws NullPointerException
table.put("key", null); // throws NullPointerException tooThis surprises people who move code from HashMap to Hashtable. A working line suddenly blows up, and the stack trace points at a plain put call.
Hashtable carries a few methods from the old days. They do the same job as the modern ones, just with different names.
Watch out for contains(). On a Map, most people expect it to check keys. On Hashtable, it checks values instead.
Honestly, no. For new code there is no good reason to reach for Hashtable. ConcurrentHashMap does the same job much better.
You will still meet it in old projects, though. Some legacy APIs hand one back, so you cannot always avoid it. Knowing how it behaves helps you read that code.
One familiar class keeps it alive too. The java.util.Properties class extends Hashtable, and Properties shows up whenever you load a config file. So every Java developer uses a Hashtable now and then, usually without noticing.
Removing Hashtable from old code is normally painless. Change the declared type, then hunt for any nulls you were storing. The rest of your calls keep working, since the method names match.
| INTERVIEW INSIGHT A common interview trap: “Hashtable is thread safe, so my code is thread safe, right?” Not quite. Each single call is atomic, but a check-then-act pair is not. Two threads can both pass a containsKey check and then both write. |
ConcurrentHashMap arrived in Java 5 to solve exactly the problem above. It gives you thread safety without making every thread wait for one lock.
Go back to the office analogy. Instead of one door, imagine the building has many doors, one per room. Two people heading to different rooms never block each other.
That is the core idea. ConcurrentHashMap locks a single bucket rather than the entire table. Threads writing to different buckets run side by side.
Java 7 did this with fixed segments, usually sixteen of them. Java 8 dropped segments and now locks the first node of a bucket directly. The lock got even smaller, so more threads fit through.
Here is the part people love. A get() call takes no lock. Readers never block, and they never block writers either.
The trick lies in the fields. Internal nodes mark their value and next pointer as volatile. That keeps updates visible to other threads without any locking.
So a read-heavy workload flies. Even with writes happening, readers keep moving.
Like Hashtable, ConcurrentHashMap rejects null keys and null values. The reason is more interesting than you might guess.
Suppose get(“x”) returns null. On a HashMap you can call containsKey(“x”) to tell the two cases apart. Did the key exist with a null value, or was it missing entirely?
In threaded code that follow-up check is useless. Another thread may change the map between your two calls. Banning nulls removes the ambiguity for good.
ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>();
scores.put("amit", 90); // fine
scores.put(null, 10); // NullPointerException
scores.put("neha", null); // NullPointerException
// Use getOrDefault when a key may be missing
int value = scores.getOrDefault("neha", 0);
System.out.println(value); // 0A single put is atomic on all three classes. Real code, though, often needs two steps. Check whether a key exists, then write it.
Those two steps together are not atomic. ConcurrentHashMap gives you single-call versions that are.
// Unsafe: two threads can both pass the check
if (!map.containsKey("hits")) {
map.put("hits", 1);
}
// Safe: one atomic call
map.putIfAbsent("hits", 1);
// Safe counting, even with many threads
map.merge("hits", 1, Integer::sum);Use these methods and most race conditions simply disappear. Skip them, and the lock inside the map will not protect your logic.
| INTERVIEW INSIGHT Expect this question: “ConcurrentHashMap is thread safe, so why did my counter still lose updates?” Because the code used get, then plus one, then put. Three separate calls. Each one is atomic, but the group is not. merge() or compute() fixes it. |
Loop over a HashMap while changing it, and you usually get a ConcurrentModificationException. The iterator notices the map moved under it and gives up.
ConcurrentHashMap behaves differently. Its iterators are weakly consistent. They keep going even while other threads edit the map.
What does that mean in practice?
One more thing worth knowing. A size() call gives you a good estimate, not a frozen truth. Under heavy writes, the number can already be stale by the time you read it.
Remember how a HashMap resize turns ugly with threads? ConcurrentHashMap solves that problem in a clever way.
When the table needs to grow, one thread starts the move. Other threads do not simply wait around. If they arrive during the transfer, they pitch in and move a chunk of buckets themselves.
Buckets already moved get marked with a special forwarding node. A thread that hits one of those follows it to the new table. Nobody reads a half-copied slot.
So the resize spreads across whatever threads happen to be around. The map stays usable the whole time, and no single thread carries the full cost.
ConcurrentHashMap keeps no order at all. Iterate over it twice and the keys may come out differently. That matches HashMap behaviour, so it should feel familiar.
Sometimes you need sorted keys and thread safety together. None of our three classes give you that combination.
For that case, Java offers ConcurrentSkipListMap. It keeps keys sorted, stays thread safe, and supports the NavigableMap methods. Lookups run in O(log n) instead of O(1), so use it only when order actually matters.
Need insertion order plus safety? Wrap a LinkedHashMap with Collections.synchronizedMap, or guard it with your own lock. There is no lock-free version in the standard library.
Here is the whole comparison in one place. Keep this handy before an interview.
| Feature | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| Thread safe | No | Yes | Yes |
| Locking style | None | Whole object | Per bucket |
| Null key | One allowed | Not allowed | Not allowed |
| Null values | Allowed | Not allowed | Not allowed |
| Introduced in | Java 1.2 | Java 1.0 | Java 5 |
| Read performance | Fastest | Slow | Fast, lock free |
| Write under load | Unsafe | Poor | Good |
| Iterator type | Fail fast | Fail fast (Iterator) | Weakly consistent |
| Throws CME | Yes | Yes | No |
| Best use | Single thread | Legacy code only | Shared, threaded code |
Notice the pattern. HashMap wins on raw speed for one thread. ConcurrentHashMap wins everywhere threads are involved. Hashtable wins nothing.
The phrase thread safe gets thrown around a lot. Let us pin down what it really means here.
Two threads reading the same map cause no trouble. Reading changes nothing, so nobody steps on anybody.
Writing is where it breaks. Say two threads add entries to the same bucket at once. Each one reads the bucket, builds a new chain, and writes it back.
Whoever writes last wins. The other entry disappears without any error message. Your map is now missing data, and nothing in the logs tells you why.
Many tutorials suggest wrapping a HashMap like this:
Map<String, Integer> map =
Collections.synchronizedMap(new HashMap<>());The wrapper does make each method thread safe. Sadly, it uses one lock for the entire map. Performance ends up close to Hashtable.
There is a second catch. Iteration is not covered by the wrapper. You must synchronize the loop yourself, or risk an exception.
synchronized (map) {
for (String key : map.keySet()) {
System.out.println(key);
}
}Forget that block and your code can fail at random. ConcurrentHashMap needs no such ceremony.
This point deserves its own heading, because it catches even senior developers. A thread-safe map protects one call, not your sequence of calls.
Look at this counter:
// Broken, even on a ConcurrentHashMap
Integer count = map.get("visits");
map.put("visits", count + 1);Two threads can read the same count. Both add one. Both write the same result, and one increment vanishes.
The fix is a single atomic call:
// Correct
map.merge("visits", 1, Integer::sum);Same idea, one operation. The map handles the locking for that bucket, and no update gets lost.
Numbers depend on your hardware, your JDK, and your workload. Still, the broad shape holds up well.
HashMap is the quickest here. No locks, no extra checks, nothing to coordinate. Pure speed.
ConcurrentHashMap comes close, since reads take no lock anyway. Hashtable trails behind, because it still grabs a lock nobody needs.
Now the order flips. ConcurrentHashMap pulls ahead, and the gap widens as you add cores.
Hashtable flattens out fast. Adding threads does not help, because they all queue at the same lock. Sometimes more threads make things worse.
HashMap is not even in this race. It might look fast for a while, then silently corrupt your data.
ConcurrentHashMap uses a bit more memory than HashMap. Extra bookkeeping fields and counter cells add up.
For most applications that overhead is tiny. Do not choose an unsafe map to save a few kilobytes.
| INTERVIEW INSIGHT If someone asks you to quote exact benchmark numbers, be careful. Say that ConcurrentHashMap scales far better under write contention, and that the actual gap depends on thread count, key spread, and JDK version. Honest reasoning beats a memorised figure. |
Time for the practical part. The decision is easier than the theory suggests.
That covers a huge share of everyday Java code. Local maps inside methods are the classic case.
Web applications hit this case constantly. Every request runs on its own thread, so any shared map needs protection.
Basically never, in new code. Keep it only when an old API hands you one and you cannot change that API.
If you inherit Hashtable code, migration is usually easy. Swap the type for ConcurrentHashMap and remove any nulls first.
Ask yourself one question. Can two threads reach this map at the same time?
If the answer is no, use HashMap and move on. Local variables, method-scoped maps, and read-only lookup tables all fall here.
If the answer is yes, or even maybe, use ConcurrentHashMap. The extra safety costs you very little, and guessing wrong costs you a lot.
Theory sticks better with a real problem. Say you run a web service and want to count hits per page.
A beginner usually writes something like this. It reads clean, and it works perfectly in local testing.
public class HitCounter {
private final Map<String, Integer> hits = new HashMap<>();
public void record(String page) {
Integer current = hits.get(page);
if (current == null) {
hits.put(page, 1);
} else {
hits.put(page, current + 1);
}
}
}Two bugs hide in there. The map itself is a HashMap, so it is not safe to share. And the read-then-write pattern loses updates even on a safe map.
Locally you have one thread, so nothing goes wrong. Deploy it, and every request runs on its own thread. Counts start drifting low.
Switch the map type, and replace those three lines with one atomic call.
public class HitCounter {
private final ConcurrentHashMap<String, Integer> hits =
new ConcurrentHashMap<>();
public void record(String page) {
hits.merge(page, 1, Integer::sum);
}
public int get(String page) {
return hits.getOrDefault(page, 0);
}
}The merge call does everything in one step. It inserts 1 when the page is new. Otherwise it adds 1 to whatever is already there.
All of that happens under the bucket lock. No other thread can slip in between the read and the write.
For extremely heavy counting, there is an even better tool. LongAdder spreads its total across several internal cells, so threads rarely collide.
Pair it with the map and you get the best of both worlds:
Map<String, LongAdder> hits = new ConcurrentHashMap<>(); hits.computeIfAbsent(page, k -> new LongAdder()).increment();
The computeIfAbsent call creates the adder once per page. After that, every hit just bumps the counter. This scales beautifully under load.
These trip up beginners and experienced developers alike. Learn them once and save yourself a long debugging session.
We covered this above, and it is worth repeating. Check-then-act patterns still race, no matter which map you use. Reach for the atomic methods instead.
The wrapper looks like a quick win. Under real traffic it becomes your bottleneck, because every call fights for one lock.
Code that works on HashMap can crash on ConcurrentHashMap. A null value slips in from a database row or an API response, and you get a NullPointerException. Clean the data before you store it.
Change a field on a key object, and its hash code changes with it. The entry now sits in the wrong bucket, so lookups fail even though the entry is right there. Use immutable keys such as String, Integer, or a class with final fields.
On ConcurrentHashMap, size() is a snapshot estimate. Do not build critical logic on that number while other threads write. Track counts with a dedicated counter if you need accuracy.
Custom key classes must override both methods, and the pair must agree. Skip that step, and two equal-looking keys will land in different buckets. All three map classes share this rule.
A: HashMap is fast but not thread safe. Hashtable is thread safe but locks the whole object, so threads queue up. ConcurrentHashMap is thread safe and locks only one bucket at a time, so many threads work in parallel.
A: On a HashMap you can call containsKey to tell a missing key apart from a key with a null value. In threaded code that second check is unreliable, because another thread can change the map in between. ConcurrentHashMap bans nulls to remove that ambiguity.
A: Yes. A HashMap that is fully built and then never modified is safe to share across threads. Problems start the moment one thread writes to it.
A: It locks a single bucket instead of the entire table. Java 7 used fixed segments, and Java 8 replaced them by locking the first node of a bucket. Reads take no lock at all, because the internal nodes use volatile fields.
A: Because the code did a get, then added one, then a put. Each call is atomic on its own, but the group of three is not. Two threads can read the same value and write the same result. Use merge or compute instead, so the whole update happens in one atomic call.
A: No. The wrapper uses a single lock for the whole map, so performance ends up close to Hashtable. It also does not cover iteration, so you must synchronize your loops manually.
A: Its iterators are weakly consistent. They reflect the map as of some point after the iteration started, and changes made during the loop may or may not appear. The loop always finishes without an exception.
A: No. ConcurrentHashMap does the same job with far better scaling. You will still meet Hashtable in legacy code, and java.util.Properties extends it, so it is worth knowing how it behaves.
A: Treat it as a good estimate, not a frozen truth. Under heavy concurrent writes, the number can already be stale by the time you read it. Use a dedicated counter when you need exact values.
A: Use ConcurrentSkipListMap. It keeps keys sorted, stays thread safe, and supports NavigableMap methods. Lookups run in O(log n) rather than O(1), so pick it only when order genuinely matters.
Let us tie it together. All three classes store key-value pairs, but they were built for different worlds.
HashMap is fast and flexible, and it belongs in single-threaded code. Nulls are welcome, locks are absent, and shared use will hurt you.
Hashtable is the ancestor. It locks everything, blocks everyone, and rejects nulls. Read it in old code, but do not write new code with it.
ConcurrentHashMap is the one to remember for threaded work. Small locks, lock-free reads, no nulls, and a set of atomic methods that keep your logic honest.
One rule sums it up nicely. Alone in a method, use HashMap. Shared across threads, use ConcurrentHashMap. Everything else is history.