一句话总结
HashMap 非线程安全(允许 null 键值,初始容量 16,2 倍扩容,扰动函数优化 hash)→ Hashtable 线程安全但已过时(synchronized 全表锁,不允许 null,初始容量 11,2n+1 扩容)。多线程用 ConcurrentHashMap(JDK 7 分段锁 → JDK 8 CAS+synchronized,锁粒度更细)。
初级理解
| 对比维度 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 不安全 | 安全(synchronized) |
| null 键/值 | 允许一个 null 键,多个 null 值 | 不允许 null 键和 null 值 |
| 继承关系 | 继承 AbstractMap | 继承 Dictionary(过时类) |
| 迭代器 | Iterator(fail-fast) | Enumerator(非 fail-fast) |
| 初始容量 | 16 | 11 |
| 扩容倍数 | 2 倍 | 2 倍 + 1 |
| hash 算法 | 扰动函数优化 | 简单取模 |
| 出现版本 | JDK 1.2 | JDK 1.0 |
中级深入
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 选择
面试模拟
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",有歧义。