深度解析 JDK1.8 ConcurrentHashMap:从底层结构到并发核心原理
一、JDK1.8 底层数据结构:彻底抛弃分段锁
JDK1.7 的 ConcurrentHashMap 采用 Segment 分段锁,锁粒度较大;而 JDK1.8 直接摒弃该设计,采用与 HashMap1.8 同源的 数组 + 链表 + 红黑树 结构,并发控制升级为 CAS + synchronized 桶级锁。
1.1 核心结构组成
1 | ConcurrentHashMap |
1.2 三大关键节点类型
- Node 基础链表节点,存储
key/val/hash/next,val和next都用volatile修饰,保证并发可见性。 - TreeBin 红黑树的包装节点,不直接存数据,只维护红黑树的根与平衡,链表长度≥8 且数组≥64 时触发转换。
- ForwardingNode 扩容专用标记节点,
hash = MOVED(-1),占位在旧数组桶中,引导其他线程协助扩容,而非阻塞等待。
1.3 并发控制三剑客
- CAS:无锁操作,用于空桶插入、数组初始化等无冲突场景。
- synchronized:仅锁定当前桶头节点,锁粒度极小,JDK1.8 后优化效果极佳。
- volatile:修饰数组与节点指针,保证多线程间的可见性,禁止指令重排。
二、put () 方法:并发插入全流程
put() 是 ConcurrentHashMap 最复杂的方法,所有并发精髓都在这里,底层由 ==putVal()== 实现。
2.1 完整执行流程
- 参数校验:
key/value不允许为 null,直接抛空指针。 - 哈希计算:通过扰动函数
spread()计算 hash,减少冲突。 - 数组初始化:
table为空时,调用initTable(),CAS 竞争初始化权。 - 桶定位:
(n-1) & hash计算数组下标。 - 三大分支处理
- 桶为空:CAS 直接插入,无锁。
- 桶为
ForwardingNode:当前正在扩容,线程协助扩容。 - 桶有数据:
synchronized锁定桶头节点,链表 / 红黑树插入。
- 树化判断:链表长度≥8 尝试转红黑树。
- 计数 + 扩容检查:
addCount()更新元素数量,达到阈值触发扩容。
2.2 核心源码解析
1 | public V put(K key, V value) { |
三、get () 方法:全程无锁,高性能读取
ConcurrentHashMap 的 get 操作完全无锁,是高并发读场景的关键优化。
3.1 执行流程
- 计算 hash,定位桶下标。
- 匹配头节点,直接返回。
- 节点为
ForwardingNode(扩容中):去新数组nextTable查询。 - 节点为
TreeBin:红黑树查找。 - 普通链表:遍历查找。
- 无结果返回 null。
3.2 源码解析
1 | public V get(Object key) { |
无锁原理:依靠
volatile保证数组和节点的可见性,读取到的永远是最新数据。
四、size () 方法:并发计数机制
JDK1.8 不再维护全局单一计数,而是采用 LongAdder 思想,避免高并发计数竞争。
4.1 计数结构
baseCount:基础计数值。CounterCell[]:并发单元格,竞争激烈时,线程分散累加不同单元格。- 总数量 =
baseCount + 所有 CounterCell 之和。
4.2 核心特点
size()返回的是估算值,非强一致(并发修改可能存在误差)。- 无需加锁统计,性能远高于加锁求和。
- 超过
Integer.MAX_VALUE时返回最大值。
五、JDK1.8 核心优化亮点
- 锁粒度极致细化 从 JDK1.7 ==分段锁(锁一段)==→ JDK1.8 ==桶头节点锁==,仅锁冲突数据。
- CAS 无锁优化 空桶插入、数组初始化全程无锁,低并发下性能接近 HashMap。
- 红黑树提升查询 链表过长时转为红黑树,查询复杂度从 O (n) → O (log n)。
- 多线程协同扩容 扩容时其他线程不阻塞,直接
helpTransfer()协助迁移,大幅提速。 - 纯无锁读 get 全程不加锁,读多写少场景并发能力拉满。
六、总结
JDK1.8 ConcurrentHashMap 是 Java 并发设计的巅峰之作:
- 结构:数组 + 链表 + 红黑树
- 并发:CAS + 桶级 synchronized + volatile
- 插入:空桶无锁、冲突加锁、扩容协助
- 读取:全程无锁,高性能
- 计数:分散单元格,减少竞争
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JIE的笔记本!
评论



