Java ConcurrentHashMap 在 JDK 1.7 vs 1.8 的核心区别:并发控制从“分段锁 Segment”变成“桶级 CAS + 少量 synchronized + 红黑树” ,结构更简单、吞吐更高、内存更省。
1) 数据结构差异
JDK 1.7:Segment + HashEntry(分段)
ConcurrentHashMap内部是Segment[]- 每个
Segment里维护一个HashEntry[](类似 HashMap 桶数组) - Segment 继承 ReentrantLock:并发靠“锁住某个 Segment”
并发粒度:Segment 级别(默认 16 个左右,最多同时 16 把锁并发写)
JDK 1.8:Node[] + CAS + TreeBin(桶级)
- 直接是
Node[] table - 桶内是链表
Node,冲突严重时树化成 红黑树 TreeBin - 并发写优先用 CAS,必要时对桶头用 synchronized(锁粒度到桶/链表/树)
并发粒度:桶级别(理论上更细)
2) 锁机制差异(最重要)
1.7:ReentrantLock(Segment 锁)
- 写操作:锁住对应 Segment
- 读操作:多数无锁(依赖 volatile 保证可见性)
优点:实现清晰
缺点:锁粒度不够细,热点段会成为瓶颈;Segment 结构额外占内存
1.8:CAS + synchronized(桶锁)
-
put时:- 桶为空:CAS 直接放入
- 桶不空:
synchronized (桶头节点)进入桶级加锁进行链表/树操作
-
使用 synchronized 的原因:JDK 8 对 synchronized 做了大量优化(偏向锁/轻量锁等)
优点:热点更分散,吞吐更好;结构更省内存
缺点:实现复杂度更高
3) 红黑树优化(1.8 新增)
- 1.7:桶冲突一直是链表,极端情况下退化 O(n)
- 1.8:链表长度超过阈值会 树化,查询接近 O(log n)
常见规则(记住大概即可):
- 链表长度到 8 触发树化意向
- table 容量至少 64 才真正树化(否则优先扩容)
4) 扩容机制差异
1.7:Segment 内各自扩容
- 扩容发生在 Segment 维度,锁住 Segment 后 rehash
- 扩容影响范围较小,但总体结构重
1.8:全表扩容 + 多线程协助迁移
- 引入
sizeCtl、transferIndex等控制字段 - 扩容时多个线程可以“帮忙搬迁桶”(协同 transfer),降低单线程扩容抖动
- 桶迁移后会放一个
ForwardingNode标记“已迁移”
5) 计数方式差异(size 统计)
1.7:
- 维护 Segment 级 count
size()需要汇总多个 Segment,可能重试(弱一致)
1.8:
- 使用 baseCount + CounterCell(类似 LongAdder 分散热点)
- 高并发下计数冲突更小,性能更稳
6) 小结对比表
| 维度 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 结构 | Segment[] + HashEntry[] | Node[] + 链表/红黑树 |
| 锁粒度 | Segment 锁(ReentrantLock) | 桶级 CAS + synchronized |
| 读 | 基本无锁 | 基本无锁 |
| 写 | 进入 Segment 锁 | 空桶 CAS;非空桶 synchronized |
| 冲突优化 | 链表 | 链表→红黑树 |
| 扩容 | Segment 内扩容 | 全表扩容,多线程协助迁移 |
| 计数 | Segment 汇总 | baseCount + CounterCells |