本文目录导读:

ConcurrentHashMap 的分段锁机制是 Java 7 及之前版本中的核心设计(Java 8 之后改用 CAS + synchronized + 红黑树 来优化锁粒度),分段锁的核心思想是:将数据分片,分别加锁,从而提高并发效率。
下面先详细解释 Java 7 的分段锁原理,再说明 Java 8 的改进。
Java 7 中的分段锁(Segment)
数据结构
- 整个
ConcurrentHashMap由一个 Segment 数组 构成。 - 每个
Segment本身就是一个 小型 HashMap(内部包含一个HashEntry数组)。 - 每个
Segment继承自ReentrantLock(可重入锁),即每个 Segment 拥有自己的锁。
示意图:
ConcurrentHashMap
├── Segment[0] (锁1)
│ ├── HashEntry[0]
│ ├── HashEntry[1]
│ └── ...
├── Segment[1] (锁2)
│ ├── HashEntry[0]
│ └── ...
├── ...
└── Segment[15] (锁16) // 默认16个Segment
锁的粒度
- 默认并发级别:16(即 16 个 Segment)。
- 锁的粒度:一个锁只保护一个 Segment 内的所有 HashEntry。
- 不同的 Segment 之间 完全独立,可以同时被不同线程访问。
读写操作流程
put 操作
// 简化逻辑
public V put(K key, V value) {
int hash = hash(key);
int segmentIndex = (hash >>> segmentShift) & segmentMask;
Segment<K,V> s = segments[segmentIndex];
// 对单个 Segment 加锁(可重入锁)
s.lock();
try {
// 在 Segment 内部的 HashEntry 数组中插入或更新
// 每个 Segment 内部与 HashMap 类似
} finally {
s.unlock();
}
}
- 只锁一个 Segment,其他 15 个 Segment 完全不受影响,可被其他线程并发操作。
get 操作
public V get(Object key) {
int hash = hash(key);
int segmentIndex = (hash >>> segmentShift) & segmentMask;
Segment<K,V> s = segments[segmentIndex];
// get 操作不加锁,依靠 volatile 关键字保证可见性
// HashEntry 的 value 字段是 volatile 的
HashEntry<K,V> e = s.getEntry(hash, key);
return e == null ? null : e.value;
}
- get 不加锁,因此读取效率极高。
- 通过
volatile保证:读线程一定能看到其他线程写入的最新值。
size 操作
- 需要获取所有 Segment 的大小之和。
- 不加锁的尝试:先不加锁累加两次。
- 如果两次结果一致,则直接返回。
- 如果不一致,则 锁住所有 Segment 再计算。
分段锁的优势
| 特性 | 普通 HashMap(线程不安全) | Hashtable / Collections.synchronizedMap | ConcurrentHashMap(分段锁) |
|---|---|---|---|
| 锁粒度 | 无 | 整张表一把锁 | 每个 Segment 一把锁 |
| 并发读写 | 线程不安全 | 完全串行,效率低 | 不同 Segment 可并发 |
| 读操作 | 无锁 | 加锁 | 无锁(volatile) |
核心优势:在多线程环境下,多个线程可以同时写入不同的 Segment,极大提升吞吐量。
Java 8 的改进:放弃分段锁
Java 8 对 ConcurrentHashMap 进行了重构,不再使用 Segment,而是改为:
新的数据结构
- 内部直接使用 Node 数组 + 链表 / 红黑树(与 HashMap 类似)。
- 使用 synchronized + CAS 实现并发控制。
锁粒度更细
- 锁的粒度:从“一个 Segment” 降为 “一个桶(bucket)”(即数组的一个位置)。
- 当多个线程操作不同的哈希桶时,可以完全并发,无需竞争同一把锁。
实现方式
- put 操作:
- 如果桶为空:使用 CAS 无锁插入。
- 如果桶不为空:对该桶的 头节点加 synchronized 锁,然后遍历链表/红黑树。
- get 操作:完全无锁,通过 volatile 保证可见性。
- 扩容:支持 多线程协助扩容(transfer),而不是像 Java 7 那样锁住整个 Segment。
为什么比分段锁更好?
- 锁粒度更细:分段锁最多 16 个并发写入(默认),Java 8 理论上可以支持所有桶同时写入(取决于哈希分布)。
- 内存占用更小:不需要维护 Segment 数组和额外的锁对象。
- 查询效率更高:红黑树将最坏情况从 O(n) 降为 O(log n)。
总结与对比
| 版本 | 锁机制 | 锁粒度 | 并发写入上限 | 读操作锁 |
|---|---|---|---|---|
| Java 7 | Segment 继承 ReentrantLock | 每个 Segment | 默认 16 | 无锁(volatile) |
| Java 8 | CAS + synchronized | 每个哈希桶 | 理论所有桶 | 无锁(volatile) |
- Java 7 分段锁:把 Map 分成若干段,每段一把锁,不同段可并发。
- Java 8 改进:去掉分段,直接用 CAS 和桶级锁(synchronized),粒度更细、性能更好、内存更低。