返回 Java / Kotlin
Java / Kotlin
7 分钟阅读

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 时:

  1. 根据 hash 定位到属于哪个 Segment
  2. 只锁这个 Segment
  3. 其他 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 是:

  1. 算 key 的 hash
  2. 定位 Segment
  3. 获取该 Segment 的锁(ReentrantLock
  4. 在这个 Segment 内部的链表/桶里插入或更新
  5. 释放锁

多个线程操作不同 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 本身慢,是那一把锁把所有人都堵死了。

相关文章

Java / Kotlin
8 分钟
HashMap in Java
Java 7 的数组+链表,Java 8 的红黑树——我自己翻笔记整理的一份 HashMap 底层笔记。
Java / Kotlin
8 分钟
Java Lock
分段锁之前的那几棒——synchronized、ReentrantLock、CAS 与 AQS 的地图。