HashMap、Hashtable、ConcurrentHashMap 核心对比
一、整体概览
- HashMap :单线程高性能,线程不安全
- Hashtable :古老集合,全表 synchronized,线程安全但效率极低
- ConcurrentHashMap :线程安全,分段锁 / CAS + synchronized,高并发首选
二、核心区别速查表
| 对比项 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ 不安全 | ✅ 安全 | ✅ 安全 |
| 锁机制 | 无锁 | 方法全加 synchronized |
JDK7:分段锁 Segment JDK8:CAS + synchronized |
| 效率 | 最高 | 最低 | 高(并发性能远胜 Hashtable) |
| key/value 是否允许 null | key/value 都可 null | key/value 都禁止 null | key/value 都禁止 null |
| 继承 | 继承 AbstractMap | 继承 Dictionary | 继承 AbstractMap |
| 默认容量 | 16 | 11 | 16 |
| 扩容 | 容量 ×2 | 容量 ×2+1 | 同 HashMap |
| 哈希冲突 | JDK7:数组+链表 JDK8:数组+链表+红黑树 | 数组+链表 | JDK8:数组+链表+红黑树 |
| 迭代器 | 快速失败 fail-fast | 快速失败 fail-fast | 安全失败 fail-safe |
三、详细说明
1. HashMap(最常用)
- 单线程环境下性能最好
- 线程不安全,高并发会出现数据丢失、死循环
- key 和 value 都可以为 null(只能有一个 key 为 null)
- JDK8 优化:链表长度 ≥8 转为红黑树,≤6 退化为链表
- 扩容机制:达到负载因子 0.75 时扩容为原来 2 倍
2. Hashtable(基本废弃)
- 线程安全靠 方法级别 synchronized
- 锁整个哈希表,并发能力极差
- 不允许 null key / null value
- 历史遗留类,不推荐使用
3. ConcurrentHashMap(高并发标准方案)
JDK 1.7
- 采用 Segment 分段锁
- 默认 16 个 Segment,每段一把锁
- 支持 16 个线程并发
JDK 1.8(重大优化)
- 取消 Segment,直接用 数组 + 链表 + 红黑树
- 锁粒度降低:只锁链表/红黑树的头节点
- 读操作无锁,使用 CAS + volatile
- 写操作使用 synchronized + CAS
- 并发性能大幅提升
四、底层扩容机制(通用)
- 初始容量默认 16
- 负载因子默认 0.75
- 元素数量 > 容量 × 0.75 时触发扩容
- 扩容为 原容量 × 2
- 重新计算所有元素哈希位置
五、为什么 ConcurrentHashMap 不允许 null?
- 多线程场景下,
get(key) == null无法区分是 key 不存在 还是 value 为 null - 为避免歧义,严格禁止 null
HashMap 单线程无歧义,所以允许 null。
六、使用场景总结
- 单线程 → 用 HashMap
- 高并发 → 用 ConcurrentHashMap
- Hashtable → 基本废弃,不要用
七、高频面试题
-
HashMap 为什么线程不安全?
多线程同时扩容可能形成环形链表,导致死循环;同时 put 可能覆盖值。
-
ConcurrentHashMap 在 JDK7 和 JDK8 的区别?
JDK7:Segment 分段锁
JDK8:CAS + synchronized + 红黑树,锁粒度更小,性能更高。
-
为什么 HashMap 允许 null,ConcurrentHashMap 不允许?
多线程无法区分 null 是没找到还是值本身为 null。
-
HashMap 哈希冲突怎么解决?
链地址法,JDK8 加入红黑树优化。