# Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?
6 min read
Table of Contents
Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?
Java ConcurrentHashMap 1.7 与 1.8 核心区别详解
【核心定义】
ConcurrentHashMap 是 Java 并发包中线程安全的哈希表实现,1.7 版本采用分段锁(Segment)机制保证并发安全,而 1.8 版本则改为基于 CAS + synchronized 的细粒度锁优化。
【关键要点】
-
数据结构差异
- 1.7:数组 + 分段锁(Segment) + 链表
- 1.8:数组 + 链表/红黑树 + CAS + synchronized
-
并发控制机制
- 1.7:分段锁(ReentrantLock),默认 16 个 Segment,每个 Segment 独立加锁
- 1.8:CAS + synchronized 锁单个桶(Node),锁粒度更细
-
哈希冲突处理
- 1.7:仅使用链表解决哈希冲突
- 1.8:链表长度 ≥ 8 且数组长度 ≥ 64 时转为红黑树,提升查询效率
-
扩容策略
- 1.7:各 Segment 独立扩容,互不影响
- 1.8:支持多线程协同扩容,效率更高
-
统计机制
- 1.7:size() 方法采用分段统计,可能不准确
- 1.8:引入 CounterCell 数组,基于 LongAdder 思想实现精确统计
【深度推导/细节】
1.7 版本分段锁设计原理
- 设计动机:避免对整个哈希表加锁,将锁粒度控制在 Segment 级别
- Segment 结构:继承 ReentrantLock,包含一个 HashEntry 数组
- 并发度控制:默认 16 个 Segment,最大支持 65535 个
- 锁竞争场景:不同 Segment 的操作完全并行,同一 Segment 的操作串行
1.8 版本 CAS + synchronized 优化
- CAS 应用场景:
- 插入新节点时(头节点为 null)
- 扩容时创建新数组
- 计数统计
- synchronized 锁升级:
- 只锁住单个桶(Node)
- 配合链表转红黑树机制
- 扩容协同机制:
- 线程发现需要扩容时,协助其他线程一起迁移数据
- 通过 ForwardingNode 标记已迁移的桶
红黑树转换临界点分析
- 链表转红黑树:链表长度 ≥ 8
- 红黑树转链表:树节点数 ≤ 6
- 数组长度要求:必须 ≥ 64 才进行转换
- 设计合理性:
- 8 和 6 之间设置 2 的缓冲区间,避免频繁转换
- 小数组时优先扩容而非转树,空间效率更高
【关联/对比】
ConcurrentHashMap vs HashMap
| 特性 | HashMap | ConcurrentHashMap 1.7 | ConcurrentHashMap 1.8 |
|---|---|---|---|
| 线程安全 | 否 | 是(分段锁) | 是(CAS+synchronized) |
| 空键值 | 允许 | 不允许 | 不允许 |
| 迭代器 | fail-fast | 弱一致性 | 弱一致性 |
| 性能 | 最高 | 中等 | 接近 HashMap |
ConcurrentHashMap 1.7 vs 1.8 性能对比
- 读操作:1.8 完全无锁,性能显著优于 1.7
- 写操作:低并发时 1.8 更优,高并发时各有优势
- 内存占用:1.8 更节省内存(无 Segment 对象开销)
- 扩容效率:1.8 多线程协同扩容效率更高
『面试官追问』
可能追问的问题:
-
为什么 1.8 要放弃分段锁?
- 分段锁在极高并发下仍可能成为瓶颈
- synchronized 在 Java 1.6 后性能大幅优化
- CAS 操作更适合现代多核 CPU
-
ConcurrentHashMap 的 size() 方法如何保证准确性?
- 1.7:尝试多次统计,最多重试 3 次,仍不一致则锁住所有 Segment
- 1.8:基于 CounterCell 的分段计数,类似 LongAdder 实现
-
1.8 版本如何避免死锁?
- 锁只加在单个 Node 上,锁范围极小
- 扩容时采用头插法,不会形成循环依赖
-
ConcurrentHashMap 的 key 和 value 能否为 null?
- 不能为 null,设计上避免二义性(无法区分是不存在还是值为 null)
-
扩容时如何保证线程安全?
- Step 1:创建新数组(2倍大小)
- Step 2:遍历旧数组,为每个桶创建 ForwardingNode
- Step 3:多线程协助迁移数据
- Step 4:迁移完成后更新引用
版本差异总结表:
| 对比维度 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 锁粒度 | Segment 级别 | Node 级别 |
| 锁实现 | ReentrantLock | synchronized + CAS |
| 数据结构 | 数组+链表 | 数组+链表/红黑树 |
| 查询复杂度 | O(n) | O(log n) 最坏情况 |
| 扩容方式 | 分段独立扩容 | 协同扩容 |
| 统计方式 | 分段统计 | CounterCell 数组 |
| 内存占用 | 较高 | 较低 |
关键数字解析:
- 16:1.7 默认 Segment 数,也是 1.8 的默认初始容量
- 0.75:默认负载因子,空间与时间的平衡点
- 8:链表转红黑树的阈值
- 6:红黑树转链表的阈值
- 64:最小树化容量,避免小表过度复杂化
这种设计演进体现了 Java 并发编程从粗粒度锁向细粒度、无锁化方向的发展趋势,1.8 版本在保持线程安全的同时,大幅提升了并发性能。