#ConcurrentHashMap JDK 7 分段锁
面试官问:"JDK 7 的 ConcurrentHashMap 是怎么实现线程安全的?"
候选人小汤答:"用分段锁。"
面试官追问:"具体是怎么实现的?"
小汤说:"分成多个 Segment..."
面试官追问:"每个 Segment 相当于什么?"
小汤答不上来。
【面试官心理】 JDK 7 的分段锁是 ConcurrentHashMap 的经典设计。能说出 Segment 继承 ReentrantLock、16 个分段、并发度为 16 的候选人,说明对 JDK 7 并发集合有了解。
#一、分段锁设计 🔴
// JDK 7 的 ConcurrentHashMap 结构
public class ConcurrentHashMap<K, V>
extends AbstractMap<K, V>
implements ConcurrentMap<K, V> {
// 默认分段数:16
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// Segment 数组(不可扩容)
final Segment<K, V>[] segments;
// Segment 继承 ReentrantLock
static final class Segment<K, V> extends ReentrantLock {
transient volatile HashEntry<K, V>[] table;
transient int count;
// ...
}
}#1.1 数据结构图
ConcurrentHashMap (JDK 7)
┌─────────────────────────────────────────┐
│ Segment[] (16 个,分段锁) │
│ ┌─────┬─────┬─────┬─────┬─────┬───┐ │
│ │ Seg0│ Seg1│ Seg2│ Seg3│ ... │Seg15│ │
│ └──┬──┴──┬──┴──┬──┴──┬──┴─────┴───┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ Hash│ │ Hash│ │ Hash│ │ Hash│ ← 链表 │
│ │Entry│ │Entry│ │Entry│ │Entry│ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────┘
并发度:最多 16 个线程同时写入(理想情况)#二、put 流程 🔴
public V put(K key, V value) {
// 1. 计算 key 的 hash
int hash = hash(key);
// 2. 定位 Segment
int segIdx = (hash >>> 28) & (segmentMask); // segmentMask = 15
Segment<K, V> segment = segments[segIdx];
// 3. 对 Segment 加锁
segment.lock();
try {
// 4. 在 Segment 内部操作 HashMap
HashEntry<K, V>[] tab = segment.table;
int index = (tab.length - 1) & hash;
HashEntry<K, V> first = tab[index];
// 5. 遍历链表查找/插入
for (HashEntry<K, V> e = first; ; ) {
if (e != null) {
if (e.key == key || key.equals(e.key)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
e = e.next;
} else {
// 插入新节点
tab[index] = new HashEntry<>(key, value, first);
break;
}
}
} finally {
// 6. 解锁
segment.unlock();
}
}#三、JDK 7 vs JDK 8 对比 🟡
| 维度 | JDK 7 分段锁 | JDK 8 CAS+synchronized |
|---|---|---|
| 锁粒度 | Segment 级别(粗粒度) | Node 级别(细粒度) |
| 并发度 | 固定 16(可配置) | 动态,取决于实际数据分布 |
| 读操作 | 无锁 | 无锁(volatile) |
| 写操作 | 加锁 Segment | 首次 CAS 失败后,锁单个 Node |
| 实现复杂度 | 高(双重结构) | 低(统一 Node 结构) |
| 内存开销 | 高(每个 Segment 独立结构) | 低(只有数据节点) |
#四、为什么 JDK 8 改进了 🟡
// JDK 7 分段锁的问题:
// 问题一:并发度固定
// 即使数据很少,也只能有 16 个线程并发
// 如果写入集中在 1 个 Segment,并发度降为 1
// 问题二:初始化开销
// ConcurrentHashMap 构造时会创建所有 Segment
// 问题三:内存浪费
// 每个 Segment 都有独立的 table
// 即使某个 Segment 没有数据,也占用内存
// JDK 8 改进:
// 1. 去掉 Segment,直接用 Node
// 2. CAS + synchronized 代替 ReentrantLock
// 3. 支持多线程并发扩容
// 4. 内存效率更高