ConcurrentHashMap 作为 Java 并发集合的核心组件,其线程安全的实现机制在不同版本中经历了显著优化。以下从数据结构、锁机制、读写策略、扩容机制及迭代器设计等多个维度,结合 JDK 1.7 与 JDK 1.8 的实现差异,详细解析其线程安全原理。
一、数据结构与锁机制的演进
1. JDK 1.7 的分段锁(Segment Lock)
- 分段设计:
ConcurrentHashMap 将哈希表划分为多个段(Segment),每个段独立维护一个 HashEntry 数组(链表结构)。默认情况下,Segment 数量为 16,每个段独立加锁(基于ReentrantLock
),允许不同段的并发操作,从而降低锁竞争。 - 锁粒度优化:
例如,两个线程分别操作不同段的键值对时,无需竞争同一把锁。仅当操作同一段时,需获取该段的锁,此时其他段仍可正常访问。 - 哈希定位:
通过两次哈希计算(第一次定位段,第二次定位段内的链表头部)实现快速访问。get
操作无需加锁,依赖volatile
修饰的value
字段保证可见性;put
操作需先获取段锁,再操作链表。
2. JDK 1.8 的 CAS + synchronized 精细化锁
- 结构简化:
摒弃分段锁,采用与 HashMap 类似的 数组 + 链表/红黑树 结构。每个数组元素(桶)的首节点作为锁单位,锁粒度进一步细化至单个链表或树节点。 - 无锁化操作:
- CAS(Compare-And-Swap) :用于初始化数组、更新
sizeCtl
(控制扩容状态)等场景,避免全局锁的开销。 - synchronized:仅对当前操作的桶首节点加锁,其他桶仍可并发访问。例如,插入新节点时,若桶为空,通过 CAS 写入;若存在哈希冲突,则对首节点加
synchronized
锁后操作链表或红黑树。
- CAS(Compare-And-Swap) :用于初始化数组、更新
- 内存可见性:
关键字段如table
(哈希桶数组)、nextTable
(扩容临时数组)、Node.val
均使用volatile
修饰,确保多线程间的可见性。
二、并发读写策略
1. 读操作的无锁化
- 依赖 volatile 变量:
所有Node
的value
和next
字段均声明为volatile
,保证线程读取时直接获取最新值,无需加锁。 - 弱一致性迭代器:
迭代器(如entrySet()
)采用 fail-safe 机制,允许在遍历过程中并发修改,不会抛出ConcurrentModificationException
。其原理是迭代器基于创建时的数据快照,但可能无法反映后续修改。
2. 写操作的同步控制
- 锁分段(JDK 1.7):
写操作需获取对应段的ReentrantLock
,若锁竞争失败,通过scanAndLockForPut
自旋尝试获取锁,避免线程阻塞。 - 精细化锁(JDK 1.8):
仅锁定当前操作的桶首节点,其他桶的写入不受影响。例如,插入操作中,若桶为空,通过 CAS 写入;若存在节点,则对首节点加synchronized
锁后处理链表或树。
三、扩容机制的线程安全
1. 触发条件
- 容量阈值:
当元素总数超过容量 × 负载因子
,或单个链表长度超过 8(且数组长度 ≥64)时触发树化,否则扩容。 - 并发协助:
检测到其他线程正在扩容(通过sizeCtl
状态),当前线程会协助迁移数据,而非重复触发扩容。
2. 多线程协作扩容
- 分段迁移(JDK 1.7):
每个段独立扩容,仅迁移该段内的数据,不影响其他段的操作。 - 并发迁移(JDK 1.8):
- 任务分配:通过
transferIndex
记录当前迁移进度,线程按逆序领取迁移任务(如从数组末尾向前处理),避免竞争。 - ForwardingNode:迁移完成的桶会被标记为
ForwardingNode
,读请求直接转发至新数组(nextTable
),写请求则协助迁移。 - CAS 协调:通过 CAS 更新
sizeCtl
和transferIndex
,确保多线程扩容的原子性和进度同步。
- 任务分配:通过
3. 扩容期间的安全访问
- 读写兼容性:
读操作可直接访问旧数组或新数组;写操作若命中未迁移的桶,需先协助完成迁移再执行写入。 - 渐进式扩容:
数据迁移分批次完成,避免单次扩容造成长时间阻塞,提升响应速度。
四、其他线程安全措施
1. 哈希算法优化
- 再散列(Rehash):
使用 Wang/Jenkins 哈希变种算法,减少哈希冲突,降低锁竞争概率。 - 位运算替代取模:
通过hash & (n-1)
计算索引(n
为数组长度),提升计算效率。
2. 统计操作的原子性
- 分段计数(JDK 1.7):
每个段维护独立的count
(volatile
修饰),全局size()
需遍历所有段并累加,可能牺牲实时性。 - LongAdder 风格计数(JDK 1.8):
使用baseCount
+CounterCell[]
分散计数,通过 CAS 更新,减少竞争。
五、总结:线程安全的核心设计思想
- 降低锁粒度:
从分段锁(JDK 1.7)到单桶锁(JDK 1.8),逐步缩小锁范围,最大化并发度。 - 无锁化与 CAS:
在初始化、状态变更等场景采用 CAS,减少锁的使用。 - 内存可见性保障:
volatile
变量与synchronized
内存屏障结合,确保多线程间数据一致性。 - 协作式扩容:
多线程协同迁移数据,避免单线程瓶颈,提升扩容效率。 - 数据结构优化:
链表转红黑树(JDK 1.8+)减少查询复杂度,平衡时间与空间效率。
通过上述机制,ConcurrentHashMap 在高并发场景下既保证了线程安全,又显著提升了性能,成为 Java 并发编程中不可或缺的组件。