ConcurrentHashMap分段锁机制解析
ConcurrentHashMap通过分段锁+Segment设计实现了高并发性能,与HashTable的全对象加锁形成鲜明对比。本文用图示简洁解析ConcurrentHashMap的核心机制,以及为什么HashTable性能不佳。
前言
上篇说了 HashMap 线程不安全,那线程安全的 Map 怎么选?
HashTable 太慢,synchronized 包装也一般,ConcurrentHashMap 才是王道。
今天简单聊聊 ConcurrentHashMap 的核心设计思想。
HashTable 的性能问题
全对象加锁的弊端
HashTable 的线程安全实现很粗暴:
public synchronized V put(K key, V value) {
// 整个方法加锁
}
public synchronized V get(Object key) {
// 整个方法加锁
}
这导致的问题:
HashTable并发访问示意图:
时刻1:
┌─────────────────────────────────┐
│ HashTable 🔒 │ ← 整个对象被锁住
│ [0] [1] [2] [3] [4] [5] [6] [7] │
└─────────────────────────────────┘
↑
线程T1正在put("a", 1)
时刻2:
┌─────────────────────────────────┐
│ HashTable 🔒 │ ← T2-T5都在等待
│ [0] [1] [2] [3] [4] [5] [6] [7] │
└─────────────────────────────────┘
↑ ↑ ↑ ↑
线程T1 T2 T3 T4
put("a",1) get("x") get("y") put("z",9)
进行中... 等待 等待 等待
结果:即使操作不同位置,也要排队等待!
性能瓶颈:
- 读写互斥:get 时不能 put
- 写写互斥:同时只能有一个 put
- 锁粒度太粗:整个表都被锁住
- 吞吐量低:高并发时大量线程阻塞
ConcurrentHashMap 的分段锁
核心设计理念
ConcurrentHashMap(JDK 1.7)的巧妙之处在于分而治之:
分段锁设计图:
整体结构:
┌─────────┬─────────┬─────────┬─────────┐
│Segment0 │Segment1 │Segment2 │Segment3 │ ← 多个独立的段
│ 🔒 │ 🔒 │ 🔒 │ 🔒 │ 各自有锁
└─────────┴─────────┴─────────┴─────────┘
每个Segment内部:
Segment0:
┌───┬───┬───┬───┐
│[0]│[1]│[2]│[3]│ ← 类似小HashMap
├───┼───┼───┼───┤
│ A → B │ C │ D │ ← 链表结构
└───┴───┴───┴───┘
Segment1:
┌───┬───┬───┬───┐
│[4]│[5]│[6]│[7]│
├───┼───┼───┼───┤
│ E │ F │ G │ H │
└───┴───┴───┴───┘
并发访问优势
ConcurrentHashMap并发访问:
同一时刻可以有多个操作:
┌─────────┬─────────┬─────────┬─────────┐
│Segment0 │Segment1 │Segment2 │Segment3 │
│ 🔒 │ │ 🔒 │ │
└─────────┴─────────┴─────────┴─────────┘
↑ ↑
T1操作 T3操作
put("a",1) put("x",9)
时刻分析:
T1: put("a",1) → hash到Segment0 → 锁住Segment0
T2: get("b") → hash到Segment1 → 无锁直接读
T3: put("x",9) → hash到Segment2 → 锁住Segment2
T4: get("y") → hash到Segment3 → 无锁直接读
结果:4个线程可以并发执行!
性能提升:
- 锁粒度细化:只锁相关的 Segment
- 读写分离:读操作无锁(大部分情况)
- 并发度可控:Segment 数量决定最大并发数
- 热点分散:不同 key 分布在不同段
JDK 1.8 的改进
JDK 1.8 对 ConcurrentHashMap 做了重大改进:
JDK 1.7 vs JDK 1.8:
JDK 1.7 (Segment分段锁):
┌─────────┬─────────┬─────────┬─────────┐
│Segment0 │Segment1 │Segment2 │Segment3 │ ← 固定段数
│ 🔒 │ 🔒 │ 🔒 │ 🔒 │ 并发度受限
└─────────┴─────────┴─────────┴─────────┘
JDK 1.8 (CAS + synchronized):
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ ← 数组更大
│ │ 🔒 │ │ 🔒 │ │ │ 锁更细粒度
└─────┴─────┴─────┴─────┴─────┴─────┘
↑ ↑
只锁链表头 只锁链表头
优势:
- 取消Segment概念,直接锁数组元素
- 并发度等于数组长度,理论上更高
- 结合CAS操作,减少锁竞争
- 支持红黑树优化
性能对比总结
三种Map的并发性能对比:
并发读取:
┌────────────┬──────────┬──────────┬───────────────┐
│ 操作 │HashTable │ JDK1.7 │ JDK1.8 │
│ │ │ConcHashMap│ ConcHashMap │
├────────────┼──────────┼──────────┼───────────────┤
│ 4个线程读 │ 🔒 │ ✅✅ │ ✅✅✅ │
│ 并发度 │ 1 │ 4 │ 更高 │
│ 锁竞争 │ 严重 │ 较少 │ 最少 │
└────────────┴──────────┴──────────┴───────────────┘
并发写入:
HashTable: [T1] → [T2] → [T3] → [T4] (串行)
ConcHashMap: [T1,T3] + [T2,T4] (分段并行)
JDK1.8版: [T1,T2,T3,T4] (更细粒度)
适用场景
选择指南:
HashMap:
✅ 单线程
✅ 确定无并发
❌ 多线程环境
HashTable:
✅ 简单的线程安全需求
✅ 读写操作都很少
❌ 高并发场景
ConcurrentHashMap:
✅ 高并发读写
✅ 读多写少场景
✅ 对性能有要求的多线程应用
❌ 单线程(有额外开销)
小结
ConcurrentHashMap 的核心思想是分治:
JDK 1.7 的 Segment 分段锁:
- 将大 Map 分成多个小 Segment
- 每个 Segment 独立加锁
- 大幅提升并发度
JDK 1.8 的优化:
- 取消 Segment,直接锁数组元素
- CAS + synchronized 混合使用
- 并发度更高,性能更好
与 HashTable 的区别:
- HashTable:全对象锁,并发度=1
- ConcurrentHashMap:分段锁,并发度=段数
性能提升的关键:
- 锁粒度细化
- 读写分离
- 热点分散
- 无锁优化
这就是为什么高并发场景下,ConcurrentHashMap 是首选的线程安全 Map 实现。
