ConcurrentHashMap 在 JDK 8 中弃用分段锁,改用 CAS + synchronized 组合机制,以降低锁粒度、减少竞争、提升吞吐量;底层采用 Node 数组+链表/红黑树结构,插入时仅对单个桶加锁,get 全程无锁,扩容支持多线程协作迁移。
ConcurrentHashMap 在 JDK 8 中彻底舍弃分段锁(Segment),转而采用
CAS + synchronized
的组合机制,核心目标是进一步降低锁粒度、减少线程竞争、提升高并发场景下的吞吐量。
为什么分段锁被舍弃
分段锁在 JDK 7 中虽比 Hashtable 全表锁更优,但仍存在明显瓶颈:
Segment 数量固定(默认 16),无法随实际并发压力动态伸缩,低负载时浪费锁资源,高负载时仍可能多线程争抢同一 Segment
即使只操作一个桶(Bucket),也要锁定整个 Segment(含多个链表),锁范围过大
size()、clear() 等全表操作需遍历全部 Segment 并加锁累加,时间复杂度为 O(16),且易因多次重试导致性能抖动
难以适配 JDK 8 引入的链表树化(红黑树)结构——Segment 锁无法自然覆盖树节点级别的细粒度操作
JDK 8 的新结构基础:Node 数组 + 链表/红黑树
底层回归与 HashMap 类似的单层数组结构(
),每个数组槽位(bin)独立管理:
空槽位插入:直接用 CAS 原子写入新 Node,完全无锁
非空槽位插入:仅对该槽位的首节点(即链表头或树根)加
,锁范围精确到单个桶
链表转红黑树后,同步块仍作用于树根节点,保证树操作的原子性,不扩大锁边界
CAS 与 synchronized 各司其职
二者分工明确,避免“一刀切”加锁:
CAS 用于无竞争或低冲突场景
:如初始化 table、设置扩容控制变量
、更新计数器 baseCount、插入首个节点等
synchronized 仅用于哈希冲突后的临界区
:当发现目标 bin 已有节点(
且未处于扩容中),才对
加锁,执行链表追加或树插入逻辑
所有共享字段(如
、
)均声明为
,保障可见性,配合 CAS 实现安全读写
扩容机制也更轻量
不再依赖 Segment 级别扩容,而是数组级迁移:
扩容由多个线程协作完成:首个线程通过 CAS 设置
标记开始扩容;后续线程检测到该标记,自动加入协助迁移
每个线程负责迁移特定区间桶,迁移完成后将对应位置设为
(hash = -1),其他线程遇到即跳转至新表
get 操作全程无锁,即使在扩容中,也能通过
无缝读取新旧表数据
Node[] table synchronizedsizeCtlf != nullfvalnextvolatilesizeCtl = -1ForwardingNodeForwardingNode