软件技术 · 知识点深化 · 并发编程
并发容器 ConcurrentHashMap:从分段锁到 CAS+synchronized
HashMap 并发扩容会死循环,HashTable 一把锁锁全表太慢。ConcurrentHashMap 就是为并发而生:JDK7 用分段锁 Segment,JDK8 抛弃 Segment 改用 CAS + synchronized 锁住桶头节点,读完全无锁。这一页讲透它的演进、size 怎么算、为什么禁用 null。
① 怎么学(4 步走,约 70 分钟)
先建立直觉再抠细节,按这四步走最稳:
1看图建立直觉(10 分钟)
读②③:先在脑子里画出本课核心结构图。
2记完整体系(15 分钟)
读④:把对比表和公式看懂,不要急着背。
3跟例题走一遍(20 分钟)
精读⑤:看三个例题怎么用知识点解题。
4刷题纠错(剩余时间)
做⑦⑩,错题回⑥诊断。
本课小目标学完你要能:① 对比 ConcurrentHashMap 与 HashMap/HashTable;② 说出 JDK7 分段锁与 JDK8 桶锁区别;③ 解释 size 怎么并发统计;④ 说明 key/value 为什么不能是 null。
② 一图看懂:ConcurrentHashMap 演进全景
读法:中心是本课主题,四条分支展开核心维度,下方是典型应用与高频易错点。
③ 本质直觉:ConcurrentHashMap 就是"分柜台售票"
HashMap 像一个窗口排大队,HashTable 更狠——全窗口只开一个闸门,所有人等一把钥匙(synchronized 锁整个 table)。
JDK7 的 ConcurrentHashMap 把柜台切成 N 段(默认 16),每段一把锁。不同段的写操作互不阻塞,并发度 = 段数。
JDK8 干脆连段都不要了:数组每个桶的第一个节点用 synchronized 锁住,CAS 尝试插入空桶。锁粒度从"段"细化到"桶",并发度更高。读操作因为 value 是 volatile,完全不加锁。
size 怎么算?直接数全表要加锁,太重。JDK8 用 CounterCell 数组分散计数:竞争时不同线程往不同 Cell 里加,最后 sum 求和。这和 LongAdder 是同一套思想。
为什么 key/value 不能是 null如果 map.get(key) 返回 null,你无法区分是"key 不存在"还是"key 对应的值就是 null"。在并发场景下这个二义性是致命的,所以 ConcurrentHashMap 直接禁止。
④ 完整知识体系
三种 Map 对比与关键机制
| 对比项 | HashMap | ConcurrentHashMap |
| 线程安全 | 否 | 是 |
| 锁粒度(JDK8) | 无 | 桶头节点 synchronized + CAS |
| null key/value | 允许 | 禁止 |
| 读操作 | 无锁 | 无锁(volatile) |
| size 统计 | 直接计数 | CounterCell 分散求和 |
| 扩容 | 单线程 transfer | 多线程协助扩容 |
put 流程hash -> 定位桶 -> 空桶 CAS 插入 -> 非空 synchronized 锁头节点后插入
size()baseCount + 所有 CounterCell 求和,竞争大时分散到 Cell
树化阈值链表长度 ≥ 8 且数组长度 ≥ 64 转红黑树
JDK8 多线程协助扩容
一个线程扩容时,其他线程 put 发现正在 transfer,会帮忙搬一部分桶(每个线程搬一段 stride)。这就是为什么 ConcurrentHashMap 扩容比 HashMap 快得多。
为什么 JDK8 抛弃 Segment
Segment 是 ReentrantLock 可重入锁,粒度还是太粗。JDK8 把锁直接降到桶头节点,锁竞争概率更低,且 CAS 无锁处理空桶,吞吐更高。
⑤ 应用场景与例题
例1 为什么 HashMap 多线程 put 会死循环?
JDK7 头插法扩容时链表成环,get 时死循环。
JDK7 扩容用头插,并发迁移时两个线程可能形成环形链表。JDK8 改尾插法已修复,但仍不推荐并发用 HashMap。
例2 ConcurrentHashMap 读要不要加锁?
为什么读快?
读操作不加锁。Node 的 val 和 next 都是 volatile,保证多线程写时读能看到最新值。只在 compute/merge 等复合操作才加锁。
例3 size() 是强一致还是弱一致?
size() 时另一个线程在 put,结果准吗?
弱一致。size() 是 baseCount + CounterCell 的近似和,不加全局锁,可能在统计过程中有新写入。这是为了性能牺牲强一致。
做题心法锁粒度越细越好:从全表→段→桶,每一步都是性能提升。
⑥ 高频错误诊断(4 条)
错误1:用 HashMap 做缓存并发 put 扩容可能死循环(JDK7)或数据丢失,必须用 ConcurrentHashMap。
错误2:以为 ConcurrentHashMap 读也要锁读完全无锁,靠 volatile 保证可见性。
错误3:put 一个 null value直接抛 NPE。key 和 value 都不允许 null。
错误4:以为 size() 精确size() 是弱一致的近似值,并发下不保证精确。
⑦ 考点真题演练(5 题)
考点分布
| 考法 | 出题形式 | 应对 |
| JDK8 锁 | 问锁粒度 | synchronized 桶头节点 |
| null | 问为什么禁止 | 二义性 |
| size | 问怎么统计 | CounterCell 分散求和 |
| 树化 | 问什么时候转红黑树 | 链表≥8且数组≥64 |
真题basic1. JDK8 ConcurrentHashMap 写操作加锁的粒度是?
真题mid2. ConcurrentHashMap 为什么不允许 null value?
真题mid3. JDK8 ConcurrentHashMap 读操作是否加锁?
真题hard4. ConcurrentHashMap 的 size() 是怎么实现的?
真题hard5. 链表转红黑树的阈值是?
⑧ 必背知识点卡
JDK7:Segment 分段锁,并发度=段数
JDK8:CAS 空桶 + synchronized 锁头节点 锁粒度更细
读:volatile 无锁
null:key/value 都禁止 二义性
size:CounterCell 分散计数,弱一致
扩容:多线程协助搬桶 比 HashMap 快
树化:链表≥8 且数组≥64 转红黑树
⑨ 动手输出:向面试官对比三种 Map
场景:面试官问"HashMap、HashTable、ConcurrentHashMap 区别"。
① HashMap:非线程安全,允许 null,单线程最快。
② HashTable:线程安全但 synchronized 锁全表,性能差,已淘汰。
③ ConcurrentHashMap:JDK8 用 CAS+synchronized 锁桶,读无锁,并发性能最好。
④ null:HashMap 允许,ConcurrentHashMap 禁止,因为二义性。
⑤ 选型:单线程 HashMap;并发读多写少用 ConcurrentHashMap。
口述思路锁粒度从全表到段到桶,每一步都在提升并发度。
⑩ 分层练习(基础 + 中档 + 拔高)
▍基础 6 题
基础1ConcurrentHashMap 锁的是什么?
JDK8 锁桶头节点。
基础2key 可以是 null 吗?
不可以,value 也不可以。
基础3读操作需要加锁吗?
不需要,靠 volatile。
基础4HashTable 为什么慢?
synchronized 锁整个 table。
基础5JDK7 用什么分段?
Segment 数组,默认 16 段。
基础6树化阈值是多少?
链表≥8 且数组≥64。
▍中档 5 题
中档1为什么 JDK8 抛弃 Segment?
Segment 粒度还是粗,桶级锁并发度更高。
中档2size() 是强一致吗?
不是,是弱一致近似值。
中档3多线程扩容怎么协助?
其他线程发现正在 transfer 会帮忙搬桶段。
中档4CounterCell 思想类似哪个类?
LongAdder,分散竞争。
中档5ConcurrentHashMap 允许 put 空值吗?
不允许,直接 NPE。
▍拔高 5 题
拔高1为什么读不加锁还能保证可见性?
Node 的 val/next 是 volatile。
拔高2CAS 插入空桶失败怎么办?
自旋重试或退化为加锁。
拔高3红黑树退化回链表的条件?
扩容时如果树节点≤6 退化为链表。
拔高4ConcurrentHashMap 迭代器是强一致吗?
弱一致,迭代过程中可能反映未完成的修改。
拔高5为什么 ConcurrentHashMap 不能用于统计总数精确值?
size() 不加全局锁,是近似和。
⑪ 记忆口诀 + 7 天复习计划
三句口诀① 分段到桶锁演进,CAS 空桶省开销。② 读无锁靠 volatile,null 二义性禁止。③ size 分散 CounterCell,弱一致换吞吐。
| 天 | 任务 | 自检 |
| 第 1 天 | 读②③,画 JDK8 结构图 | 桶锁说清 |
| 第 2 天 | 背三 Map 对比表 + 基础 6 题 | null 规则记住 |
| 第 3 天 | 做中档 5 题,解释 size 原理 | CounterCell 说清 |
| 第 4 天 | 做拔高 5 题,对比 JDK7/JDK8 | 演进能讲 |
| 第 5 天 | 做⑦真题 5 题 | 限时每题 2 分钟 |
| 第 6-7 天 | 合书口述读为什么无锁 | 不看资料 |
← 返回软件技术总览