HashMap 和 Hashtable 的区别?

2025年 阅读约 6 分钟 面试指南 · Java面试

深入解析HashMap和Hashtable的区别,从线程安全到null值处理,分初级、中级、高级三个层次全面讲解。

一句话总结

HashMap 非线程安全(允许 null 键值,初始容量 16,2 倍扩容,扰动函数优化 hash)→ Hashtable 线程安全但已过时(synchronized 全表锁,不允许 null,初始容量 11,2n+1 扩容)。多线程用 ConcurrentHashMap(JDK 7 分段锁 → JDK 8 CAS+synchronized,锁粒度更细)。

初级理解

对比维度HashMapHashtable
线程安全不安全安全(synchronized)
null 键/值允许一个 null 键,多个 null 值不允许 null 键和 null 值
继承关系继承 AbstractMap继承 Dictionary(过时类)
迭代器Iterator(fail-fast)Enumerator(非 fail-fast)
初始容量1611
扩容倍数2 倍2 倍 + 1
hash 算法扰动函数优化简单取模
出现版本JDK 1.2JDK 1.0
// HashMap 允许 null Map<String, String> map = new HashMap<>(); map.put(null, "value"); // OK map.put("key", null); // OK // Hashtable 不允许 null Hashtable<String, String> table = new Hashtable<>(); table.put(null, "value"); // NullPointerException table.put("key", null); // NullPointerException

中级深入

Hashtable 的线程安全实现:几乎所有方法都用 synchronized 修饰,是全表锁,并发性能极差。多线程环境下不推荐使用。

HashMap 为什么允许 null 键?HashMap 对 null 键做了特殊处理,hash 值固定为 0,放在数组的第 0 个桶。Hashtable 不允许 null 是因为它直接调用 key.hashCode(),null 会抛 NPE。

Hashtable 的替代方案:需要线程安全的 Map 时,应该用 ConcurrentHashMap(分段锁/CAS,性能远优于 Hashtable)或 Collections.synchronizedMap()

高级拓展

Hashtable 的扩容为什么是 2n+1?Hashtable 的容量不要求是 2 的幂,使用 % 取模。2n+1 是为了让容量尽量是素数或奇数,减少 hash 碰撞(因为偶数取模时低位相同的 key 容易碰撞)。

ConcurrentHashMap 为什么取代了 Hashtable?

JDK 7 ConcurrentHashMap 使用分段锁(Segment),默认 16 个段,每个段独立加锁,并发度 16。

JDK 8 ConcurrentHashMap 改用 CAS + synchronized,锁粒度更细(只锁桶的头节点),并发性能大幅提升。

// 线程安全的 Map 选择 // 1. Hashtable — 不推荐,全表锁性能差 // 2. Collections.synchronizedMap — 全表锁,不推荐 Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>()); // 3. ConcurrentHashMap — 推荐,分段锁/CAS ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
面试加分项:能说出 ConcurrentHashMap 在 JDK 7 和 JDK 8 的实现差异,说明你对并发集合有深入理解。

实战场景

场景:多线程环境下的 Map 选择

// 场景1:读多写少 → ConcurrentHashMap ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>(); // get 不加锁,put 只锁桶头节点,并发性能极高 // 场景2:全部线程共享一个 Map → ConcurrentHashMap // 场景3:每个线程独立 Map → HashMap(ThreadLocal 隔离) ThreadLocal<Map<String, Object>> threadLocalMap = ThreadLocal.withInitial(HashMap::new); // 场景4:初始化后不再修改 → HashMap + Collections.unmodifiableMap Map<String, String> config = Collections.unmodifiableMap(new HashMap<>() {{ put("key1", "value1"); put("key2", "value2"); }});

面试模拟

Q:Hashtable 和 ConcurrentHashMap 有什么区别?

A:Hashtable 用 synchronized 全表锁,所有操作串行化,并发性能极差。ConcurrentHashMap JDK 7 用分段锁(16 个 Segment),JDK 8 用 CAS + synchronized 锁桶头节点,读操作完全无锁,写操作只锁一个桶,并发性能远超 Hashtable。

Q:为什么 Hashtable 不允许 null 键值?

A:Hashtable 直接调用 key.hashCode() 和 value 相关方法,null 会抛 NPE。HashMap 对 null key 做了特殊处理(hash 固定为 0,放第 0 个桶)。更深层的原因是:ConcurrentHashMap 也不允许 null,因为在并发环境下,get 返回 null 无法区分是"key 不存在"还是"value 就是 null",有歧义。