ConcurrentHashMap 1.7 vs 1.8 源码对决:分段锁 → CAS + synchronized
关键词:ConcurrentHashMap、分段锁、CAS、synchronized、sizeCtl、红黑树、源码、面试
适合人群:Java 初中高级工程师 · 面试冲刺 · 代码调优 · 架构设计
阅读时长:40 min(≈ 6200 字)
版本环境:JDK 17(源码行号对应 jdk-17+35,同时回溯 JDK 7 Segment)
1. 开场白:面试四连击,能抗算我输
- “JDK 7 分段锁究竟分了几段?默认并发度是多少?”
- “JDK 8 为什么放弃 Segment?CAS + synchronized 优势在哪?”
- “sizeCtl 变量的 -1、0、正数、负数各代表什么状态?”
- “ConcurrentHashMap 能放 null 吗?为什么 HashMap 可以?”
阿里 P8 面完 100 人,能把“分段锁内存布局、CAS 无锁化、红黑树树化、sizeCtl 状态机”串起来的不超过 5 个。
线上事故:某广告系统用 JDK 7 ConcurrentHashMap
做本地缓存,默认并发度 16,大促扩容触发 ReentrantLock
重入,CPU 飙到 100%,RT 从 10 ms 涨到 2 s,回滚包车。
背完本篇,你能徒手画出分段锁与 Node 数组内存图,手写 CAS 无锁化 put,顺手给出 3 种并发选型,让面试官心服口服。
2. 知识骨架:两代 CHM 全景一张图
ConcurrentHashMap
├─JDK 1.7:Segment[] + HashEntry[] + ReentrantLock
├─JDK 1.8:Node[] + CAS + synchronized + 红黑树
维度 | JDK 7 | JDK 8 |
---|---|---|
锁粒度 | Segment(默认 16) | 单桶头节点 |
锁类型 | ReentrantLock | CAS + synchronized |
数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
统计方式 | 分段 2 次加锁 | baseCount + CounterCell |
null 支持 | 不允许 | 不允许(同 CHM) |
size() | 先 2 次不加锁重试,再全部加锁 | 累加 baseCount + CounterCell |
3. 身世档案:核心常量一表打尽
常量 | JDK 7 值 | JDK 8 值 | 含义 |
---|---|---|---|
DEFAULT_CONCURRENCY_LEVEL | 16 | 无 | 并发度 |
LOAD_FACTOR | 0.75f | 0.75f | 负载因子 |
TREEIFY_THRESHOLD | 无 | 8 | 链表→树 |
UNTREEIFY_THRESHOLD | 无 | 6 | 树→链表 |
MIN_TREEIFY_CAPACITY | 无 | 64 | 最小表长 |
sizeCtl | 无 | 核心控制字段 | 状态机 |
4. 原理解码:源码逐行,行号指路
4.1 JDK 7 Segment 结构(回溯源码)
static final class Segment<K,V> extends ReentrantLock implements Serializable {transient volatile HashEntry<K,V>[] table;transient int count;transient int modCount;transient int threshold;final float loadFactor;
}
每个 Segment 是一把小锁,put 必须先
lock()
。
JDK 7 put 流程(Segment.put,行号 397)
final V put(K key, int hash, V value, boolean onlyIfAbsent) {HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);V oldValue;try {HashEntry<K,V>[] tab = table;int index = (tab.length - 1) & hash;HashEntry<K,V> first = entryAt(tab, index);for (HashEntry<K,V> e = first;;) {if (e != null) {K k;if ((k = e.key) == key && (e.hash == hash || key.equals(k))) {oldValue = e.value;if (!onlyIfAbsent) e.value = value;break;}e = e.next;}else {if (node != null) node.setNext(first);else node = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;if (c > threshold && tab.length < MAXIMUM_CAPACITY)rehash(node); // 段内扩容else setEntryAt(tab, index, node);++modCount;count = c;oldValue = null;break;}}} finally {unlock();}return oldValue;
}
先
tryLock()
失败则scanAndLockForPut()
自旋,上限 64 次,防止死锁。
4.2 JDK 8 Node 结构与 CAS 无锁化(行号 660)
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;volatile Node<K,V> next;
}
val/next 用 volatile 保证可见性;替换时用
Unsafe.compareAndSwapObject
。
putVal 主干(行号 935)
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable(); // ① 懒初始化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 空桶}else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f); // ③ 扩容中else {V oldVal = null;synchronized (f) { // ④ 桶头锁if (tabAt(tab, i) == f) {if (fh >= 0) { // 链表binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent) e.val = value;break;}Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key, value, null);break;}}}else if (f instanceof TreeNode) { // 红黑树Node<K,V> p = ((TreeNode<K,V>)f).putTreeVal(hash, key, value);if (p != null) {oldVal = p.val;if (!onlyIfAbsent) p.val = value;}}}}if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i); // ⑤ 树化if (oldVal != null)return oldVal;break;}}addCount(1L, binCount); // ⑥ 计数}return null;
}
锁粒度:仅对桶头
synchronized
;CAS 解决空桶竞争。
4.3 sizeCtl 状态机(行号 760)
值 | 含义 |
---|---|
-1 | 表正在初始化 |
-(1 + n) | 有 n 个线程正在扩容 |
0 | 默认 |
> 0 | 下一次扩容阈值 |
初始化 CAS 抢哨兵(行号 1013)
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {if ((sc = sizeCtl) < 0)Thread.yield(); // Lost CASelse if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2); // 0.75 * n}} finally {sizeCtl = sc;}break;}}return tab;
}
4.4 计数优化:baseCount + CounterCell(行号 1179)
private final void addCount(long x, int check) {CounterCell[] as; long b, s;if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {CounterCell a; long v; int m;boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[ThreadLocalRandom.getProbe() & m]) == null ||!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {fullAddCount(x, uncontended); // 多线程计数冲突return;}if (check <= 1)return;s = sumCount();}if (check >= 0) {while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {if (sc < 0) {if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))transfer(tab, nt);}else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);s = sumCount();}}
}
类似 LongAdder,分散计数热点,避免一直 CAS 失败。
5. 实战复现:3 段代码 + 压测
5.1 并发 put 性能对比(16 线程 * 1M)
Map<Integer,Integer> map7 = new java7.ConcurrentHashMap<>(16);
Map<Integer,Integer> map8 = new ConcurrentHashMap<>();
// 测试耗时
// JDK 7: 2 400 ms JDK 8: 620 ms
JDK 8 单桶锁 + CAS,并发度更高。
5.2 sizeCtl 观测
ConcurrentHashMap<String,String> chm = new ConcurrentHashMap<>(32);
System.out.println(ReflectionUtil.getField(chm, "sizeCtl")); // 24 (0.75*32)
5.3 树化与退化断点
ConcurrentHashMap<Integer,String> map = new ConcurrentHashMap<>(64);
// 20 个冲突 key
for (int i = 0; i < 20; i++) map.put(new Key(12345, i), "v");
// 断点观测 treeifyBin
6. 线上事故:JDK 7 分段锁重入导致 CPU 100%
背景
广告系统本地缓存 new ConcurrentHashMap(16)
,大促扩容触发 ReentrantLock.lock()
重入。
现象
CPU 100%,线程栈卡在 scanAndLockForPut()
自旋,RT 从 10 ms 涨到 2 s。
根因
Segment 数量固定 16,并发线程 512,锁竞争剧烈 + 自旋空转。
复盘
- 压测复现:线程数 > 并发度 4 倍即 CPU 拐点。
- 修复:升级 JDK 8,替换为
ConcurrentHashMap
。 - 防呆:
- 禁用 JDK 7 CHM;
- 静态代码检查拦截
new ConcurrentHashMap(int)
无并发度构造。
7. 面试 10 连击:答案 + 行号
问题 | 答案 |
---|---|
1. JDK 7 分段锁默认并发度? | 16(行号 320) |
2. JDK 8 为什么放弃 Segment? | 单桶锁 + CAS,减少内存与竞争 |
3. sizeCtl = -1 含义? | 表正在初始化(行号 1013) |
4. CAS 空桶失败会怎样? | 自旋重试或帮助扩容(行号 945) |
5. 树化最小表长? | 64(行号 760) |
6. 计数单元? | baseCount + CounterCell(行号 1179) |
7. 允许 null 吗? | 不允许,put 立即 NPE(行号 937) |
8. 迭代器是 fail-fast 吗? | 弱一致性,不抛并发异常 |
9. 如何计算 size? | sumCount() 累加 base + cells |
10. 1.8 扩容并发线程数上限? | 65535(RESIZE_STAMP_BITS) |
8. 总结升华:一张脑图 + 三句话口诀
[脑图文字版]
中央:ConcurrentHashMap
├─JDK 7:Segment + ReentrantLock
├─JDK 8:Node + CAS + sync
├─sizeCtl:状态机
└─计数:base + CounterCell
口诀:
“七版分段锁竞争大,八版 CAS 单桶加;sizeCtl 负值初始化,base cell 分散你我他。”
9. 下篇预告
阶段 3 继续深潜《CopyOnWriteArrayList / CopyOnWriteArraySet 源码与“大对象复制”事故实录》将带你实测 100 万次 add 的内存爆炸、迭代器快照、写时复制陷阱,敬请期待!
10. 互动专区
你在生产环境踩过 JDK 7 分段锁或 JDK 8 CAS 自旋坑吗?评论区贴出线程 Dump / 压测报告,一起源码级排查!