主题
概念卡片:ConcurrentHashMap 演进
一句话机制
ConcurrentHashMap 是线程安全的 HashMap,演进主线是「锁粒度变细」:1.7 用 Segment 分段锁(默认 16 段,锁住一段),1.8 改为「CAS + 局部 synchronized」(锁单个 Node 桶)+ 红黑树 + 多线程扩容。 它是「锁/CAS 知识在容器上的落地」,也是并发集合的面试核心。
1.7 → 1.8 对比
| 维度 | JDK1.7 | JDK1.8 |
|---|---|---|
| 数据结构 | Segment 数组(每段是带锁的 HashEntry 数组) | 数组 + 链表 + 红黑树(同 HashMap) |
| 加锁机制 | Segment 分段锁(继承 ReentrantLock) | CAS + synchronized(锁 Node) |
| 锁粒度 | 段(一段一个锁,默认 16 段) | 单个桶(Node) |
| 查询 | 两次 hash(先定位 Segment 再定位桶) | get 无锁(volatile + Unsafe) |
| 扩容 | 段内 rehash | 多线程协同扩容(ForwardingNode 标记) |
| 计数 | 遍历 Segment 求和(可能全加锁) | counterCells 分散热点(同 LongAdder) |
1.7 分段锁核心
- Segment:继承 ReentrantLock,每个 Segment 是一个独立的 HashEntry 数组
- 默认并发度 16:最多 16 个线程同时写不同段
- get:两次 hash + Unsafe 直接取,无锁
- size:先不加锁循环统计,连续 2 次相等才返回;超限则强制锁全部 Segment 统计
局限:锁粒度在「段」,同一段内的写仍然串行;且段数固定(构造时指定),无法随扩容动态增加。
1.8 核心机制
CAS + synchronized(锁 Node)
java
// 桶为空 → CAS 直接插入,无锁
// 桶非空 → synchronized 锁住桶头节点,再遍历插入- 读(get)完全无锁:Node 数组
volatile修饰保证数组引用可见,元素用 Unsafe/CAS 保证可见性 - 写(put):空桶 CAS 插入,非空桶锁 Node 头节点,锁粒度最小
sizeCtl(控制初始化/扩容)
| sizeCtl 值 | 含义 |
|---|---|
| 0 | 数组未初始化 |
| -1 | 正在初始化(单线程 CAS 抢占) |
| -n | 有 n-1 个线程正在帮助扩容 |
| 正数 | 下一个扩容阈值 |
多线程协同扩容
- ForwardingNode:hash = -1 的标记节点,扩容时放在桶上,其他线程 put 发现它就去帮助迁移数据(helpTransfer)
- 解决「单线程扩容太慢」的问题,多核机器能并行扩容
counterCells 计数
类比 LongAdder:不竞争单个 size 变量,而是把不同线程分散到不同 CounterCell 上计数,size() 时求和。避免高并发下 ++size 的竞争。
与 HashMap 的关系
CHM 1.8 的底层结构与 HashMap 1.8 一致(数组+链表+红黑树),差异在于并发控制:HashMap 无锁(线程不安全),CHM 用 CAS + synchronized 保证安全,代价是内存多占(sizeCtl/counterCells/ForwardingNode 等额外字段)。
常见误解(避坑)
- ❌ "CHM 的 get 也要加锁"。→ 1.8 的 get 完全无锁,靠 volatile + Unsafe 保证可见性,读性能极高。
- ❌ "CHM 1.7 分段锁粒度就是最优了"。→ 1.8 用 CAS + synchronized 把锁粒度从「段」缩到「单个桶」,并发度更高。
- ❌ "CHM 不允许 null key/value"。→ 是的,key 和 value 都不能为 null(HashMap 允许),因为并发下无法区分「不存在」和「值为 null」。
- ❌ "CHM 扩容是单线程的"。→ 1.8 通过 ForwardingNode 实现多线程协同扩容,put 遇到扩容中的桶会帮忙迁移。
关联
- 原始资料:ConcurrentHashMap1.7 · ConcurrentHashMap1.8
- 总览:Java集合框架技术栈总览
- 相关卡:概念卡片:HashMap底层原理 · 概念卡片:CAS乐观锁与原子类
- 域地图:A00-百科/Java后端/Java后端