在-JDK-1.8-的-ConcurrentHashMap-中,为什么存在两种插入方式
目录
在 JDK 1.8 的 ConcurrentHashMap 中,为什么存在两种插入方式?
在 JDK 1.8 的
ConcurrentHashMap
中,之所以对“容器为空”和“计算位置为空”采取不同的处理方式,主要是因为
并发场景下的性能优化和并发安全保证
。我们可以分开来看这两种情况:
1. 容器为空时,使用 volatile
+ CAS
初始化
- 原因
:
ConcurrentHashMap
采用 懒加载 ,并不会在构造时就初始化所有桶(Node<K, V>[] table
)。 - 实现
:当第一次插入元素时,会先判断
table
是否为空:
if (tab == null || (n = tab.length) == 0)
tab = initTable();
initTable()
方法使用 CAS(Compare-And-Swap) 操作来保证线程安全的初始化。为什么用 CAS 而不是
synchronized
?- 目的是减少不必要的锁竞争,提高并发性能。
- 由于初始化操作通常只需要执行一次(典型的 双重检查锁 模式),CAS 在多数情况下不会失败,所以开销较小。
2. 计算出的位置为空时,使用 CAS 插入
原因 :如果某个桶(即
table[index]
)位置为空,说明没有哈希冲突,我们可以直接尝试插入数据。实现 :使用
CAS
方式直接插入:if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null))) { break; // 插入成功,退出循环 }
为什么用 CAS 而不是
synchronized
?因为这个位置是
null
,没有竞争,所以可以直接尝试用 无锁的 CAS 操作 插入,避免加锁的开销,提高性能。
3. 计算出的位置不为空时,使用 synchronized
原因 :如果
table[index]
位置已经有元素了,可能会遇到 哈希冲突 ,需要遍历该链表或红黑树进行替换或追加。实现 :
- 先通过
synchronized
锁住该桶(synchronized (f)
)。 - 然后遍历这个桶:
- 如果 key 已存在 ,则更新 value。
- 如果 key 不存在 ,则添加新的节点(链表 or 红黑树)。
- 插入完成后, 判断链表长度是否达到阈值(8),如果达到就转换为红黑树 。
- 先通过
为什么用
synchronized
而不是 CAS?- CAS 只能保证单个变量的原子性,而不能保证整个链表或树结构的原子性 。
- 当多个线程同时修改一个桶时,直接用
synchronized
保护整个桶的操作,避免复杂的 CAS 失败重试,提高效率。
JDK 1.8 在
ConcurrentHashMap
中通过
分阶段使用 CAS 和 synchronized
,既保证了
高并发性能
,又保证了
线程安全
,这就是它不同情况下采用不同方式的原因。