HashMap、Hashtable 和 ConcurrentHashMap 的区别?
HashMap、Hashtable 和 ConcurrentHashMap 的区别?
zhangchangsheng面试考察点
- 线程安全认知:面试官不仅仅是想知道这三者谁是线程安全的,更是想考察你是否理解不同线程安全实现方式的性能差异,以及为什么
Hashtable已经被淘汰。 - 并发编程深度:考察你是否了解
ConcurrentHashMap的底层实现原理,包括 JDK 7 的分段锁和 JDK 8 的 CAS +synchronized两种方案的演进。 - 生产实践能力:能否根据实际场景(读多写少、高并发等)选择合适的 Map 实现,避免盲目使用导致的性能问题或线程安全隐患。
核心答案
三者都是 Map 接口的实现类,但线程安全性和性能差异显著:
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ 不安全 | ✅ 安全 | ✅ 安全 |
| 锁粒度 | 无锁 | 整表锁(粗粒度) | 桶级别锁(细粒度) |
| null 键/值 | ✅ 都允许 | ❌ 都不允许 | ❌ 都不允许 |
| 初始容量 | 16 | 11 | 16 |
| 扩容倍数 | 2 倍 | 2n + 1 | 2 倍 |
| 迭代器 | 快速失败 | 快速失败 | 弱一致性 |
| 底层结构(JDK 8) | 数组 + 链表/红黑树 | 数组 + 链表 | 数组 + 链表/红黑树 |
| 性能 | 最高(单线程) | 最低(全表锁) | 高(并发优秀) |
| 适用场景 | 单线程或外部同步 | 已淘汰 | 高并发多线程 |
一句话总结:单线程用 HashMap,多线程用 ConcurrentHashMap,Hashtable 已被淘汰(历史遗留类)。
深度解析
一、线程安全实现对比
flowchart TB
subgraph HT["Hashtable:整表锁(全表同步)"]
direction LR
H["桶0 ~ 桶10 全部被同一把 synchronized 大锁锁住"]
end
HT --> T1["一把大锁锁住整个表,任何操作都会阻塞其他线程<br/>并发性能差"]
T1 --> CHM
subgraph CHM["ConcurrentHashMap(JDK 8):桶级别锁"]
direction LR
C0["桶0:锁0"] --- C1["桶1:锁1"] --- C2["桶2:锁2"] --- C3["桶3:锁3"] --- C4["……"]
end
CHM --> T2["每个桶独立加锁,并发访问不同桶互不影响<br/>高并发性能优异"]
上图展示了两种线程安全实现的核心差异:
- Hashtable:使用
synchronized修饰几乎所有方法,一把大锁锁住整个哈希表。任何时刻只允许一个线程操作,即使是访问不同位置的元素也会相互阻塞,并发性能极差。 - ConcurrentHashMap (JDK 8):采用桶级别细粒度锁,每个桶(数组位置)独立加锁。线程 A 操作索引 2 的元素,线程 B 操作索引 5 的元素,两者互不影响,可并发执行。
二、ConcurrentHashMap 的演进
JDK 7:分段锁(Segment)
// JDK 7 的分段锁设计 |
分段锁将整个 Map 分成多个段(默认 16 个),每段一把 ReentrantLock。并发度最高为分段数。 JDK 8:CAS + synchronized
// JDK 8 的桶级别锁设计 |
JDK 7 vs JDK 8 对比:
| 对比项 | JDK 7 分段锁 | JDK 8 CAS + synchronized |
|---|---|---|
| 锁粒度 | 分段级别(默认 16 段) | 桶级别(更细) |
| 并发度 | 最大为段数 | 理论上为数组长度 |
| 内存占用 | 高(多个 Segment 对象) | 低(单一数组) |
| 锁类型 | ReentrantLock | synchronized + CAS |
| 扩容 | 分段独立扩容 | 协同扩容(多线程协助) |
三、HashMap 的线程安全问题
HashMap 在多线程环境下有严重问题: 1. JDK 7 扩容死循环
// JDK 7 扩容时的头插法会导致链表成环 |
2. JDK 8 数据丢失
// JDK 8 使用尾插法避免了死循环,但仍存在数据丢失问题 |
四、null 键值处理差异
// HashMap - 允许 null |
为什么不允许 null? 在多线程环境下,get(key) 返回 null 存在二义性:
- key 不存在
- key 存在但 value 为 null
单线程的HashMap可以用containsKey()判断,但多线程下判断和获取之间存在竞态条件。
五、迭代器特性
// HashMap/Hashtable - 快速失败(Fail-Fast) |
六、最佳实践
// ❌ 错误:多线程使用 HashMap |
面试高频追问
- ConcurrentHashMap 在 JDK 7 和 JDK 8 中的实现有什么区别?
- JDK 7:分段锁(
Segment+ReentrantLock),并发度受分段数限制 - JDK 8:CAS +
synchronized桶级别锁,并发度更高,内存占用更低
- JDK 7:分段锁(
- 为什么 ConcurrentHashMap 不允许 null 键和 null 值?
- 多线程环境下
get()返回null存在二义性(不存在 vs 值为 null) - 避免在
containsKey()判断和get()获取之间出现竞态条件
- 多线程环境下
- ConcurrentHashMap 的扩容是怎样实现的?
- JDK 8 采用多线程协同扩容:每个线程负责一部分桶的迁移
- 通过
transferIndex协调分配任务,支持并发扩容提升效率
常见面试变体
- “HashMap 在多线程环境下会有什么问题?”
- “为什么 Hashtable 被淘汰了?”
- “ConcurrentHashMap 如何保证线程安全?”
记忆口诀
选择口诀:
单线程用 HashMap:性能最高,允许 null
多线程用 ConcurrentHashMap:细粒度锁,高并发
Hashtable 别用了:全表锁,已淘汰
锁粒度记忆:Hashtable = 大锅饭(一把锁)
ConcurrentHashMap (JDK 7) = 分餐制(分段锁)
ConcurrentHashMap (JDK 8) = 自助餐(桶级别锁)
总结
HashMap 非线程安全但性能最高,适合单线程;Hashtable 使用全表 synchronized 锁,性能差已被淘汰;ConcurrentHashMap 是多线程首选,JDK 8 采用 CAS + synchronized 桶级别锁实现高并发,迭代器弱一致性,不允许 null 键值。生产环境多线程场景必须使用 ConcurrentHashMap。
声明:本文转载自小哈的博客 《HashMap、Hashtable 和 ConcurrentHashMap 的区别?》,版权归原文作者所有。转载请保留本声明与原文链接。
