All three store key-value pairs using hashing, but they differ in thread safety, performance and null handling. HashMap is not thread-safe and is the default choice. Hashtable is an old, fully synchronized class. ConcurrentHashMap is the modern thread-safe map, designed for high concurrency.
Key Points: • HashMap: not synchronized, fastest in single-threaded code, allows one null key and many null values. • Hashtable: every method is synchronized on the whole table, so only one thread can use it at a time. It allows no null keys or values, and it is a legacy class from Java 1.0. • ConcurrentHashMap: thread-safe without locking the whole map. Since Java 8, it uses CAS operations and locks only individual buckets on writes, and reads don't lock at all. • ConcurrentHashMap doesn't allow nulls, because null would be ambiguous ("no mapping" or "mapped to null") in concurrent code. • Iterators: HashMap and Hashtable iterators are fail-fast. ConcurrentHashMap's iterators are weakly consistent and never throw ConcurrentModificationException. • Use Collections.synchronizedMap() only when you need a synchronized wrapper around another Map type.
Code Example:
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MapComparison {
static void tryNull(String name, Map<String, String> map) {
try {
map.put(null, "value");
System.out.println(name + ": null key allowed");
} catch (NullPointerException e) {
System.out.println(name + ": null key NOT allowed");
}
}
public static void main(String[] args) throws InterruptedException {
tryNull("HashMap", new HashMap<>());
tryNull("Hashtable", new Hashtable<>());
tryNull("ConcurrentHashMap", new ConcurrentHashMap<>());
// 4 threads x 10,000 increments on the same key
Map<String, Integer> counts = new ConcurrentHashMap<>();
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int t = 0; t < 4; t++) {
pool.submit(() -> {
for (int i = 0; i < 10_000; i++) counts.merge("hits", 1, Integer::sum); // atomic
});
}
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("ConcurrentHashMap count: " + counts.get("hits"));
}
}Output:
HashMap: null key allowed Hashtable: null key NOT allowed ConcurrentHashMap: null key NOT allowed ConcurrentHashMap count: 40000
Good to Know: Thread-safe methods don't make compound actions thread-safe. "if (!map.containsKey(k)) map.put(k, v)" can still race on a ConcurrentHashMap. Use the atomic methods putIfAbsent(), computeIfAbsent() and merge() instead, as in the example.
Interview Tip: A concise interview answer is:
"HashMap isn't thread-safe but is the fastest and allows one null key. Hashtable is a legacy class that synchronizes every method on the whole table and allows no nulls. ConcurrentHashMap is the modern thread-safe choice: it locks only individual buckets on writes, reads without locking, and doesn't allow nulls. For compound updates I use its atomic methods like merge() and computeIfAbsent()."
Previous question: What are the initial capacity and load factor of a HashMap, and what happens when it resizes?
Next question: What is the difference between List.of(), Collections.unmodifiableList() and Arrays.asList()?