TL;DR(30 秒扫完)
- JDK7 分段锁:16 个 Segment,每个 Segment 是 ReentrantLock + 小表,并发度上限 16
- JDK8 桶级锁:
Node[] table+ volatile 桶头 + 桶级synchronized,并发度 = 表大小 - putVal 五分支:空桶 CAS 无锁 → FORWARD_NODE 协助扩容 → key 相同覆盖 → TREEBIN 树内 → 链表尾插
- size 统计:
baseCountCAS +counterCells[]分片累加(LongAdder 思路) - 协助扩容:
transferIndex从右向左迁移、每线程 STRIDE=16 桶、FORWARD_NODE标记 - 为什么选 synchronized 不选 ReentrantLock:JDK6+ 锁升级追平 ReentrantLock;语义简单、JIT 友好、无 AQS 队列开销
关键结论
结论 AJDK8 ConcurrentHashMap 核心是"桶级 synchronized + CAS 混合"——空桶 CAS 无锁,非空桶锁桶头
结论 B读操作走 volatile 完全无锁,写冲突粒度只锁一个桶,读多写少场景性能极好
结论 Csize() 返回值是"某个瞬间的近似值",不保证强一致(LongAdder 分片累加)
结论 DJDK8 选 synchronized 的关键是 JDK6 引入锁升级让 synchronized 性能追平 ReentrantLock,且语义/JIT/内存都更优
完整讲解(费曼四步)
STEP 1 · 概念
ConcurrentHashMap 是 Java 中最常用的并发 Map,从 JDK7 到 JDK8 有重大演进。JDK7 用"分段锁"(Segment 数组 + 每个 Segment 内独立小 HashEntry 数组 + ReentrantLock),并发度上限 16。JDK8 彻底重构,去掉了 Segment,改用"桶级 synchronized + CAS 混合",配合 volatile 桶头、LongAdder 式分片计数、多线程协助扩容等机制,把并发度提升到表容量级别。STEP 2 · 大白话
JDK7 类比:想象一个图书馆,分成 16 个大阅览室(Segment),每个阅览室有自己的管理员(ReentrantLock)。你想借书,只要去对应阅览室拿管理员的钥匙就能读写,不同阅览室的读者可以同时进行。缺点:只有 16 个阅览室,超过 16 个人同时来就排队了。
JDK8 类比:图书馆拆掉了大阅览室的墙壁,改成一个巨大的开放书架(Node 数组),每个书架格子(桶)前只锁那一格。你要写的时候用智能钥匙(CAS)插到空格里,不用找管理员;格子非空时才锁那一格(synchronized),不影响旁边。统计书总量时把每个管理员的计数分开累加(LongAdder 分片),最后汇总——这就是"分片计数"。
STEP 3 · 底层
JDK7 结构
// JDK7
public class ConcurrentHashMap extends AbstractMap
implements Map, Cloneable, java.io.Serializable {
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
transient Segment[] segments;
transient int sizeCount; // 分段总和
// 每个 Segment 是独立的锁 + 小表
static final class Segment extends ReentrantLock
implements java.io.Serializable {
transient HashEntry[] table; // 小 HashEntry 数组
transient int count; // 本段 size
// ...
}
}
并发度上限:Segment 数量(默认 16),最多 16 个线程同时写。
Segment 需要 ReentrantLock 的原因:- 需要
tryLock()非阻塞尝试加锁(避免死等) - 需要
lockInterruptibly()可中断(应对长任务) - 需要公平锁选项(
new ReentrantLock(true))
JDK8 结构
// JDK8
public class ConcurrentHashMap<K,V>
extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Cloneable,
Serializable {
private transient volatile Node<K,V>[] table; // 桶数组,volatile
private transient volatile int sizeCtl; // -1: 首次初始化;<0: 扩容中
private transient volatile long baseCount; // 计数基值(LongAdder 思路)
private transient volatile CounterCell[] counterCells; // 分片计数
// 无 Segment,直接用 Node 数组
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V value; // volatile
volatile Node<K,V> next; // volatile
// ...
}
}
核心变化:- 去掉 Segment,用一个大表
- 桶头
volatile保证可见性 - value 和 next 都
volatile(读无需加锁) - 计数用 LongAdder 分片思路
putVal 完整流程(JDK8)
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ① hash 计算
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// ② 空表初始化(CAS 竞争)
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// ③ 桶为空:CAS 无锁写入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // CAS 成功,跳出
}
// ④ FORWARD_NODE:正在扩容,协助或等待
else if ((fh = f.hash) == MOVED) {
helpTransfer(tab, f);
continue;
}
// ⑤ 桶非空:synchronized 桶头
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (binCount++ >= TREEIFY_THRESHOLD) {
// 树化
if (fh >= 0) treeifyBin(tab, i);
}
else if (fh >= 0) {
// 链表尾插
for (Node<K,V> e = null, p = f;; p = p.next) {
if (p == null) {
f.next = new Node<K,V>(hash, key, value, null);
break;
}
else if (p.hash == hash &&
((k = p.key) == key ||
(key != null && key.equals(k))))
break;
}
}
else
((TreeNode<K,V>)f).putTreeVal(this, tab, hash, key, value);
}
}
}
}
addCount(1L, null);
return null;
}
putVal 五分支流程:
put(key, value)
│
├─ 桶为空 ──→ CAS 写入(无锁,fast path)
│
├─ FORWARD_NODE ──→ helpTransfer 或等待扩容
│
└─ 桶非空 ──→ synchronized(桶头)
├─ key 相同 → 覆盖 value
├─ TREEBIN → 树内 put
└─ 链表 → 尾插
│
└─ addCount(1L, null) 累加计数
size 统计(LongAdder 思路)
// 类似 LongAdder 的分片累加
private long sumCount() {
CounterCell[] cs = counterCells;
long sum = baseCount;
if (cs != null) {
for (int i = 0; i < cs.length; ++i) {
CounterCell c = cs[i];
if (c != null)
sum += c.value;
}
}
return sum;
}
// addCount 的分片累加逻辑
final void addCount(long x, CounterCell[] cells) {
long s;
CounterCell[] cs;
CounterCell c;
int h;
if ((cells == null && (s = baseCount + x) >= 0 &&
(cells = counterCells) == null) ||
// baseCount CAS 无竞争:直接更新
casBaseCount(baseCount, s = baseCount, s + x)) {
// 竞争了才分到 counterCells
if (x != 0 &&
(cells == null || cells.length == 0 || (cs = counterCells) == null ||
(c = cs[(h = threadIndex.get()) & (cs.length - 1)]) == null ||
!c.casValue(c.v, c.v + x))) {
// 初始化或扩容 counterCells
fullAddCount(x, cells, cs, c, h);
}
}
}
核心思想:- 无竞争:
baseCount单变量 CAS 直接更新 - 有竞争:降级到
counterCells[]数组分片累加 - 类似
LongAdder:写性能高,sum()时遍历累加 - size() 返回值是"某个瞬间的近似值",不保证强一致
协助扩容 helpTransfer
final Node<K,V>[] transfer(Node<K,V>[] tab, Node<K,V>[] nextTab,
Node<K,V>[] firstTab, int index, int bounded) {
int n = tab.length, stride = (n >>> 2) + (n >>> 3) + 1; // STRIDE: 每个线程负责 16 个桶(n=16 时)
if (stride < 16) stride = 16;
if (index < n) {
Node<K,V> head = null, tail = null;
int cl = 0;
do {
if (tabAt(tab, i) == null)
casTabAt(tab, i, null, new ForwardingNode()); // FORWARD_NODE 标记
else {
// 遍历链表,按 e.hash & n 拆分到 loHead/hiHead
// ...
}
if ((i += stride) >= n)
break;
} while (true);
return nextTab;
}
else {
// 已迁移完,检查是否可以完成扩容
if (firstTab != null) {
if (transferIndex <= 1) {
table = nextTab;
sizeCtl = (int)((long)(n << 1)) + 1;
}
}
}
}
协助扩容流程:
resize 触发 → sizeCtl = -n (n 是目标容量,负值表示扩容中)
│
├─ 主线程从 transferIndex (n-1) 开始迁移 16 个桶到 transferIndex-16
│
├─ 已迁移桶用 FORWARD_NODE (hash = MOVED = -1) 标记
│
├─ 其他线程调用 put 时:
│ ├─ 遇到 FORWARD_NODE → helpTransfer 协助迁移
│ └─ 读操作可 helpTransfer 或走 find 兜底查新旧两个位置
│
└─ 迁移完成 → table = nextTab,sizeCtl 恢复为正
关键点:transferIndex从右向左递减(避免和 put 的桶定位冲突)- 每个线程负责 16 个桶(STRIDE)
FORWARD_NODE是特殊的"已迁移标记"节点- 扩容期间读操作不阻塞,通过 helpTransfer 或 find 兜底
弱一致迭代器
public VolatileIterationMap {
// 迭代器基于快照,允许遍历时修改,不抛 CME
public final V[] copyEntries() {
// ...
}
}
特点:- 基于桶数组快照,遍历时允许并发修改
- 不抛
ConcurrentModificationException - 可能读到修改前后的任意快照
- 适合读多写少的高并发统计场景
JDK8 为什么选 synchronized 不选 ReentrantLock
技术演进:| 版本 | synchronized 性能 | ReentrantLock 优势 |
|---|---|---|
| JDK5 及以前 | 只有重量级锁,性能差 10x | AQS 无竞争 CAS,性能优 |
| JDK6 | 引入锁升级(偏向锁→轻量级锁→重量级锁),性能追平 | 略优 |
| JDK7 | 与 ReentrantLock 相当 | tryLock/公平锁/可中断等高级特性 |
| JDK8 | 完全等价 | 桶级锁语义简单,synchronized 更合适 |
- Segment 需要
tryLock()非阻塞尝试加锁(避免死等) - 需要
lockInterruptibly()可中断 - 需要公平锁模式(可选)
- 需要
lock(long, TimeUnit)超时加锁
- 技术演进:JDK6+ 锁升级让 synchronized 性能追平甚至超过 ReentrantLock
- 场景匹配:桶级锁竞争极短(微秒级),偏向锁/轻量级锁快速路径正好命中
- 实现简洁:无对象分配、无状态机、无手动 unlock 遗漏风险
- JIT 优化:逃逸分析可消除 synchronized(lock elimination)、锁粗化;ReentrantLock 是库实现无法穿透
- 内存占用:synchronized 用 Mark Word 内联存储锁信息(0 额外对象);ReentrantLock 要 new 对象 + AQS state + 队列
- JDK8 桶级锁不需要 ReentrantLock 的高级特性:无死等风险、无需可中断、无需公平锁
无锁状态(Mark Word 存 hashCode)
↓ 第一次加锁
偏向锁(Mark Word 存线程 ID,无竞争 CPU 直返,零开销)
↓ 出现第二个线程竞争
轻量级锁(CAS 尝试,失败则自旋最多 10 次)
↓ 竞争激烈或自旋失败
重量级锁(真实 OS mutex,线程 park)
JDK15 变化:JDK15 默认禁用偏向锁(JEP 374);JDK18 彻底移除偏向锁。原因:偏向锁撤销成本高(全局 safepoint + 重偏向),现代 JIT 已能更好地处理无竞争锁。
与 synchronized 和 ReentrantLock 的定位
| 场景 | 推荐 |
|---|---|
| 默认选择 | synchronized(简洁、JIT 友好) |
需要 tryLock() 非阻塞 | ReentrantLock |
| 需要可中断 | ReentrantLock.lockInterruptibly() |
| 需要公平锁 | ReentrantLock(true) |
| 需要超时加锁 | ReentrantLock.tryLock(timeout, unit) |
| 需要多个 Condition | ReentrantLock.newCondition() |
| 读多写少 | ReadWriteLock |
STEP 4 · 简化
一句话总结:JDK8 ConcurrentHashMap = 桶级 synchronized + CAS + volatile 桶头 + LongAdder 分片计数 + 多线程协助扩容;选 synchronized 是因为 JDK6+ 锁升级追平 ReentrantLock 且语义更简单。
延伸追问
ConcurrentHashMap JDK7 vs JDK8 的锁结构有什么变化?为什么?
JDK7 Segment(默认 16 个)→ JDK8 桶头 synchronized。Segment 是 JDK5/6 时代的产物,写多场景是瓶颈;桶级锁粒度更细;JDK6+ synchronized 已优化,性能相当
ConcurrentHashMap 的 size() 为什么可能不准?
baseCount + counterCells[] 分片累加,size() 遍历各分片时其他线程可能正在更新;返回值是"某个瞬间的近似值"。如果需要强一致,用 getMap().values() 之类的快照computeIfAbsent 在 ConcurrentHashMap 上有什么特殊行为?桶为空 CAS 写入;桶非空锁住桶头,计算+插入在同一锁内完成。注意:在 computeIfAbsent 的函数中不能再调用同一个 map 的其他方法(会死锁),也不能返回 null
JDK8 扩容时多线程如何协作?
transferIndex 从 table.length-1 递减,每次减 STRIDE=16;每个线程从 transferIndex 处开始迁移 16 个桶;已迁移桶用 FORWARD_NODE 标记;其他线程调用 put 时如果发现 FORWARD_NODE,调 helpTransfer 协助迁移或 find 兜底什么时候必须用 ReentrantLock 而不是 synchronized?
需要 tryLock() 非阻塞;需要 lockInterruptibly() 可中断;需要公平锁;需要多个 Condition 条件变量;需要 lock(long, TimeUnit) 超时加锁
为什么 JDK8 桶级锁不需要 ReentrantLock 的高级特性?
桶级锁竞争极短(微秒级),无阻塞死等风险;不需要可中断(迁移是短任务);不需要公平锁(吞吐优先);单个桶一个条件就够了
ConcurrentHashMap 和 Collections.synchronizedMap 怎么选?
高并发选 ConcurrentHashMap;低并发简单场景用 synchronizedMap(但迭代仍需外层加锁)
速查表
JDK7 结构:16 个 Segment(ReentrantLock + 小表)
JDK8 结构:Node[] table(volatile)+ 桶头 synchronized + CAS
putVal 五分支:
空桶 → CAS 无锁写入
FORWARD_NODE → helpTransfer
key 相同 → synchronized 覆盖
TREEBIN → synchronized 树内 put
链表 → synchronized 尾插
size 统计:baseCount CAS + counterCells[] 分片(LongAdder 思路)
→ 返回值是"近似值"
协助扩容:
transferIndex 从右向左,STRIDE=16/线程
FORWARD_NODE 标记已迁移
读操作 helpTransfer 或 find 兜底
弱一致迭代:
基于快照,遍历中修改不抛 CME
JDK8 选 synchronized 的 5 大原因:
① JDK6+ 锁升级追平 ReentrantLock
② 桶级锁竞争短,偏向锁/轻量级锁快速路径正好命中
③ 语义简洁(无手动 unlock)
④ JIT 优化友好(逃逸分析、锁消除、锁粗化)
⑤ 内存占用小(Mark Word 内联 vs AQS 队列)
关键常量:
TREEIFY_THRESHOLD=8, MIN_TREEIFY_CAPACITY=64
MOVED hash=-1 (FORWARD_NODE)
MAXIMUM_CAPACITY=1<<30, DEFAULT_INITIAL_CAPACITY=16
关联题目
- [ ] 《ConcurrentHashMap是如何保证线程安全的?》— 2026-10-09 Round 1 Q8, ⭐⭐⭐(完全没提 CAS/volatile/协助扩容/LongAdder 分片计数)
- [ ] 《ConcurrentHashMap为什么在JDK 1.8中使用
synchronized而不是ReentrantLock》— 2026-10-09 Round 1 Q9, ⭐⭐⭐⭐(讲清 JDK6 锁升级核心,但未讲 JDK7 为什么用 ReentrantLock)
关联知识
JDK8 CHM = 桶级 synchronized + CAS + volatile 桶头 + LongAdder 分片计数 + 多线程协助扩容;去掉了 JDK7 的 Segment,并发度从 16 提升到表容量
图书馆拆掉大阅览室:每格书架独立上锁;空格子用智能钥匙(CAS)直接放;统计书数用分片累加(LongAdder);扩容时多个管理员一起搬书
✦ 记 忆 口 诀 ✦
空桶 CAS 无锁;非空 synchronized 桶头;size 分片累加;扩容 transferIndex 从右向左 STRIDE=16
关键可视化
putVal 五分支流程
flowchart TB
START["put(key, value)"] --> HASH["spread(key.hashCode())"]
HASH --> T{"table 空?"}
T -- 是 --> INIT["initTable<br/>CAS 初始化"]
T -- 否 --> EMPTY{"桶为空?"}
EMPTY -- 是 --> CAS["① CAS 写入<br/>casTabAt(tab, i, null, newNode)<br/>无锁 fast path"]
EMPTY -- 否 --> FORWARD{"FORWARD_NODE?"}
FORWARD -- 是 --> HELP["② helpTransfer<br/>协助扩容"]
FORWARD -- 否 --> SYNC["③ synchronized(桶头)"]
SYNC --> SPLIT{"节点类型"}
SPLIT -- key 相同 --> OVER["覆盖 value"]
SPLIT -- TREEBIN --> TREE["树内 put"]
SPLIT -- 链表 --> TAIL["尾插新节点<br/>链长≥8 转树"]
CAS --> COUNT["addCount(1L)<br/>LongAdder 分片"]
OVER --> COUNT
TREE --> COUNT
TAIL --> COUNTsize 统计:LongAdder 分片累加
flowchart TB
ADD["addCount(1L)"] --> C1{"baseCount<br/>CAS 无竞争?"}
C1 -- 是 --> UP["直接更新 baseCount"]
C1 -- 否 --> C2["竞争<br/>降级到 counterCells[]"]
C2 --> CELL["选 counterCells[threadIndex & (len-1)]"]
CELL --> C3{"cell CAS 成功?"}
C3 -- 是 --> DONE1["cell.value += 1"]
C3 -- 否 --> C4["初始化/扩容 counterCells<br/>fullAddCount"]
DONE1 --> SUM["sumCount()<br/>= baseCount + sum(counterCells[i].value)"]
UP --> SUM
SUM --> NOTE["返回值是'某个瞬间的近似值'<br/>不保证强一致"]协助扩容 helpTransfer
flowchart TB
START["resize 触发"] --> SET["sizeCtl = -n<br/>负值表示扩容中<br/>transferIndex = n-1"]
SET --> WORK["主线程从 n-1 开始<br/>每线程负责 16 个桶 STRIDE"]
WORK --> FORWARD["已迁移桶用 FORWARD_NODE<br/>(hash = MOVED = -1)标记"]
FORWARD --> OTHER{"其他线程调用 put/get?"}
OTHER -- put --> CHECK{"遇到 FORWARD_NODE?"}
CHECK -- 是 --> HELP["helpTransfer<br/>协助迁移"]
CHECK -- 否 --> NORMAL["正常 put"]
OTHER -- get --> FIND["find 兜底<br/>查新旧两个位置"]
HELP --> END["transferIndex <= 1<br/>table = nextTab<br/>sizeCtl = n<<1 + 1"]
NORMAL --> END
FIND --> ENDJDK7 Segment vs JDK8 桶级锁
flowchart LR
subgraph JDK7["JDK7 ConcurrentHashMap"]
J1["16 个 Segment"]
J2["每个 Segment = ReentrantLock<br/>+ 小 HashEntry 表"]
J3["并发度上限:16"]
J4["需要 tryLock/lockInterruptibly<br/>公平锁等高级特性"]
J5["内存:Segment 数组 + 16 把锁"]
end
subgraph JDK8["JDK8 ConcurrentHashMap"]
K1["Node[] table<br/>+ volatile 桶头"]
K2["桶级 synchronized + CAS"]
K3["并发度 = 表容量"]
K4["桶级锁语义简单<br/>无需 ReentrantLock 高级特性"]
K5["LongAdder 分片计数<br/>协助扩容"]
end
JDK7 -.演进.-> JDK8JDK8 为什么选 synchronized 不用 ReentrantLock?
flowchart TB WHY["5 大原因"] --> R1["① JDK6+ 锁升级<br/>偏向锁→轻量级→重量级<br/>性能追平 ReentrantLock"] WHY --> R2["② 桶级锁竞争短(微秒级)<br/>偏向锁/轻量级锁快速路径<br/>正好命中"] WHY --> R3["③ 语义简洁<br/>自动加解锁<br/>无 unlock 遗漏风险"] WHY --> R4["④ JIT 优化友好<br/>逃逸分析消除锁、锁粗化<br/>ReentrantLock 无法穿透"] WHY --> R5["⑤ 内存占用小<br/>Mark Word 内联<br/>vs AQS 队列要额外对象"] WHY2["JDK7 为什么用 ReentrantLock?"] --> R6["Segment 需要 tryLock 非阻塞<br/>lockInterruptibly 可中断<br/>公平锁 超时加锁<br/>都是 ReentrantLock 独有"]
JDK6 synchronized 锁升级四条路径
flowchart LR L0["无锁状态<br/>Mark Word 存 hashCode"] --> L1["偏向锁<br/>Mark Word 存线程 ID<br/>无竞争 CPU 直返<br/>零开销"] L1 --> L2["轻量级锁<br/>CAS 尝试<br/>失败自旋最多 10 次"] L2 --> L3["重量级锁<br/>真实 OS mutex<br/>线程 park"] L3 -.单向升级不可逆.-> END["JDK15 默认禁用偏向锁<br/>JDK18 彻底移除"]
知识关系
🔄 延伸(Extends)
red-black-tree⚡ 对比(Contrast)
synchronized vs ReentrantLock— JDK8 CHM 选 synchronized 是因为 JDK6 锁升级追平 ReentrantLock,语义简洁 + JIT 友好JDK7 用 ReentrantLock— JDK7 Segment 需要 tryLock/lockInterruptibly/公平锁等 ReentrantLock 独有特性
🎯 概念
📏 规则
⚠️ 误区
🔍 追问
✨ 口诀
共 0 张卡,点击翻面