ConcurrentHashMap(CHM) 是 Java 并发包(JUC)中线程安全的哈希表实现,相比
Hashtable
和Collections.synchronizedMap
,它通过 分段锁(JDK7) 和 CAS + synchronized(JDK8) 实现高并发访问。
1. JDK7 的 ConcurrentHashMap(分段锁)
(1) 数据结构
Segment 数组:默认 16 个 Segment(相当于 16 个独立的
HashMap
),每个 Segment 继承ReentrantLock
。HashEntry 数组:每个 Segment 内部是一个
HashEntry
数组(链表结构)。
(2) 锁机制
分段锁:不同 Segment 互不影响,并发度 = Segment 数量(默认 16)。
写操作:只需锁住目标 Segment,其他 Segment 仍可并发读写。
读操作:无锁,
volatile
保证可见性。
(3) 缺点
Segment 数量固定:扩容时只扩当前 Segment 的
HashEntry
数组,无法全局扩容。锁粒度较粗:单个 Segment 锁住整个桶链表。
2. JDK8 的 ConcurrentHashMap(CAS + synchronized)
(1) 数据结构
Node 数组:类似
HashMap
,使用Node
(链表)和TreeNode
(红黑树)。并发控制:
CAS:用于无竞争时的快速插入(如
putVal
中的tabAt
/casTabAt
)。synchronized:锁住桶的头节点(粒度更细)。
(2) 核心优化
特性 | JDK7 | JDK8 |
---|---|---|
锁粒度 | Segment(分段锁) | 桶头节点(synchronized) |
数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
扩容 | 分段扩容 | 协助扩容(多线程协同) |
哈希冲突 | 链表 | 链表或红黑树(阈值=8) |
(3) 关键操作
① put 流程
计算 key 的
hash
,定位到桶。如果桶为空,CAS 插入头节点。
如果桶不为空,synchronized 锁住头节点,处理链表或红黑树插入。
判断是否需要扩容。
② get 流程
-
无锁,直接访问
volatile
的Node
数组(保证可见性)。
③ 扩容(transfer)
多线程协助:当前线程扩容时,其他线程若访问到正在迁移的桶,会帮助迁移。
高低位拆分:类似
HashMap
,拆分成低位链表和高位链表。
3. JDK8 的线程安全实现
(1) CAS 操作
初始化数组:
sizeCtl
变量控制,通过 CAS 竞争初始化权。插入头节点:
tabAt
/casTabAt
保证原子性。
(2) synchronized 锁优化
仅锁住单个桶的头节点,不影响其他桶的并发。
锁升级:无竞争时用 CAS,竞争时用
synchronized
。
(3) size 计算
基础计数器:
baseCount
(CAS 更新)。分片计数:
CounterCell[]
(避免 CAS 竞争,类似 LongAdder)。
4. 常见面试问题
Q1: ConcurrentHashMap 为什么比 Hashtable 快?
Hashtable:全局锁(
synchronized
方法级),所有操作串行。ConcurrentHashMap:
JDK7:分段锁,16 个 Segment 并发。
JDK8:桶级锁(synchronized + CAS),并发度更高。
Q2: JDK8 的 CHM 如何解决哈希冲突?
-
链表长度 ≥8 且数组容量 ≥64 时,转为红黑树(时间复杂度
O(n)
→O(log n)
)。
Q3: CHM 的 size() 是准确的吗?
-
不绝对准确:由于并发环境下统计的是瞬时值,但实际误差可忽略。
5. 总结
版本 | 实现方式 | 优点 | 缺点 |
---|---|---|---|
JDK7 | 分段锁(ReentrantLock) | 减小锁粒度 | Segment 数量固定 |
JDK8 | CAS + synchronized | 锁粒度更细、支持红黑树、扩容优化 | 实现复杂度高 |
适用场景:
高并发读:
ConcurrentHashMap
优于Collections.synchronizedMap
。高并发写:JDK8 的 CHM 性能接近无锁结构(如
ConcurrentHashMap
vsHashMap
+ 外部锁)。
理解其原理后,可以更高效地处理并发环境下的键值存储需求。