Java 分段锁 (Segmented Lock)
ConcurrentHashMap 在 JDK 1.7 里怎么「把一把大锁拆成多把小锁」——接 HashMap 那条面试链的下一棒。
为什么写这篇
写完 HashMap 那篇之后,面试关键词链往往还会继续往下走:
HashMap → 红黑树 → ConcurrentModificationException → synchronized → AQS → CAS → ConcurrentHashMap → 分段锁 → ...
HashMap 线程不安全,多线程要安全访问 Map,最土的办法是 Collections.synchronizedMap(new HashMap<>())——整把 Map 一把锁,谁动都得等。追问到这里,下一问经常是:有没有性能更好的做法?ConcurrentHashMap 的分段锁就是经典答案之一。
我自己第一次听「分段锁」这个词,脑子里浮现的是超市收银台:不是全店只开一个窗口,而是多开几个,各排各的队。这篇就把这个思路,以及它在 JDK 1.7 / 1.8 里具体长什么样,按我自己的理解整理一下。
一把大锁的问题
多线程共享一个 HashMap,直接包 synchronized:
Map<K, V> map = Collections.synchronizedMap(new HashMap<>());锁的是整个 Map。线程 A 在 put(key1, value1),线程 B 在 get(key2)——哪怕两个 key 八竿子打不着,B 也得等 A 放锁。并发一高,这把锁就成了瓶颈。
分段锁想解决的就是这件事:别把所有人都堵在同一个门口,按数据划片,各锁各的。
核心思路:大锁拆小锁
一句话:把一个大锁拆成多把小锁,每一小段(Segment)只管自己那片桶(Bucket)。
ConcurrentHashMap (JDK 1.7 示意) ├── Segment[0] —— 管一部分 bucket,自带一把锁 ├── Segment[1] —— 管一部分 bucket,自带一把锁 ├── Segment[2] —— 管一部分 bucket,自带一把锁 └── ...
线程访问某个 key 时:
- 根据 hash 定位到属于哪个 Segment
- 只锁这个 Segment
- 其他 Segment 上的读写可以并行
并发度从「全表串行」变成「最多 segmentCount 路并行」——当然,两个 key 碰巧落在同一个 Segment 里,还是得排队,但概率比全表一把锁小多了。
JDK 1.7 的 ConcurrentHashMap 怎么做的
JDK 1.7 的 ConcurrentHashMap 内部有一个 Segment<K,V>[] segments 数组。每个 Segment 继承自 ReentrantLock,内部再维护一小片 HashEntry[]:
ConcurrentHashMap ├── Segment[0] → Lock A → HashEntry[] A ├── Segment[1] → Lock B → HashEntry[] B ├── Segment[2] → Lock C → HashEntry[] C └── ...
执行 map.put("key", value) 时,roughly 是:
- 算 key 的 hash
- 定位 Segment
- 获取该 Segment 的锁(
ReentrantLock) - 在这个 Segment 内部的链表/桶里插入或更新
- 释放锁
多个线程操作不同 Segment 时可以并行;操作同一 Segment 仍然互斥。这就是 JDK 1.7 里「分段锁」的显式实现。
JDK 1.8 之后:Segment 没了,思想还在
JDK 1.8 的 ConcurrentHashMap 移除了 Segment 结构,改成 Node + CAS + synchronized:
- 不再有一眼能看出来的
Segment[] - 对单个桶的链表或红黑树节点加
synchronized - 能用 CAS 的地方先用 CAS 减少抢锁
锁粒度比 Segment 更细——从「锁一片桶」变成「往往只锁一个桶里的头节点」。面试里说「1.8 不用分段锁了」没错;但说「完全抛弃分段思想」也不太准确,更像是 把段划得更碎,锁到必要的局部。
和 HashMap 那篇连起来看:1.7 的 ConcurrentHashMap 用 Segment + HashEntry 链表;1.8 的 ConcurrentHashMap 和 1.8 的 HashMap 一样走上了 Node / 红黑树路线,只是并发控制从分段锁换成了 CAS + 细粒度 synchronized。
三种方案怎么记
| 方案 | 锁粒度 | 特点 |
|---|---|---|
HashMap + synchronizedMap | 整表一把锁 | 简单,并发差 |
ConcurrentHashMap (JDK 1.7) | Segment 级 | 分段并行,经典「分段锁」 |
ConcurrentHashMap (JDK 1.8+) | 桶/Node 级 + CAS | 更细,Segment 结构退场 |
我自己记的时候是:超市多收银台(1.7 Segment)→ 每个货架单独上锁(1.8 Node)。类比不必较真,能帮你在面试里快速组织语言就行。
手写一个最小分段锁(帮助理解)
下面这段不是 ConcurrentHashMap 源码,只是把「按 hash 分片、每片一把锁」抽成最小样本:
public class SegmentLockDemo {
private final int segmentCount = 16;
private final Object[] locks = new Object[segmentCount];
private final Map<Integer, String>[] maps = new Map[segmentCount];
public SegmentLockDemo() {
for (int i = 0; i < segmentCount; i++) {
locks[i] = new Object();
maps[i] = new HashMap<>();
}
}
private int getSegmentIndex(Object key) {
return key.hashCode() & (segmentCount - 1);
}
public void put(Integer key, String value) {
int index = getSegmentIndex(key);
synchronized (locks[index]) {
maps[index].put(key, value);
}
}
public String get(Integer key) {
int index = getSegmentIndex(key);
synchronized (locks[index]) {
return maps[index].get(key);
}
}
}segmentCount 取 16、hashCode & (segmentCount - 1) 求下标,和 HashMap 里「容量为 2 的幂、用位运算取模」是同一套习惯。不同 key 落到不同 index,就锁不同的 locks[i],互不干扰。
生产环境别自己造这个轮子——直接用 ConcurrentHashMap。这段代码的价值是:把分段锁从概念落到「几行 synchronized」,看懂它,再去看 JDK 1.7 源码会顺很多。
收尾
分段锁本质是一种 以空间换时间 的并发策略:多维护几把锁、几片数据结构,换更高的并行度。
面试里如果被问到「ConcurrentHashMap 1.7 和 1.8 区别」,我一般会先答 1.7 显式 Segment + ReentrantLock,1.8 去掉 Segment、改用 CAS + 细粒度 synchronized;再补一句 和 HashMap 一样,底层也从纯链表演进到了链表 + 红黑树。再往深挖 AQS、CAS 的实现细节,那就是关键词链更后面几棒的事了——我多半也得翻笔记。
日常写业务,ConcurrentHashMap 用起来就行,不必每次 put 都在脑子里过一遍 Segment。但搞懂分段锁,至少你能明白:多线程 Map 慢,有时候不是 HashMap 本身慢,是那一把锁把所有人都堵死了。