ConcurrentHashMap 的 CAS 详解
ConcurrentHashMap
是 Java 提供的线程安全的 HashMap
实现,它广泛应用于高并发场景。其核心性能的实现依赖于 CAS (Compare-And-Swap) 操作来确保线程安全,同时避免锁的竞争,从而提高并发性能。
1. 什么是 CAS (Compare-And-Swap)
-
核心思想:
CAS 是一种无锁的原子操作,通过硬件指令(如 x86 的CMPXCHG
)直接支持。 -
基本流程:
- 比较某个内存位置的当前值是否与预期值相等。
- 如果相等,则更新为新的值。
- 如果不相等,则说明有其他线程修改过该值,CAS 操作失败。
-
优点:
- 无需加锁,减少线程竞争,提升性能。
-
缺点:
- 存在 ABA 问题(可以通过加版本号解决)。
- 自旋失败会造成 CPU 消耗。
2. ConcurrentHashMap 的基本结构
ConcurrentHashMap
是线程安全的哈希表,其内部采用分段(Segment
)锁和链表/红黑树结合的方式存储数据(JDK 1.8 之前是基于分段锁实现,1.8 之后结构优化)。
在 JDK 1.8 中:
ConcurrentHashMap
采用数组(Node<K,V>[]
)+ 链表 + 红黑树的结构。- 对于高并发操作,利用 CAS 和 同步块 (
synchronized
) 共同保障线程安全。
3. CAS 在 ConcurrentHashMap 中的应用
ConcurrentHashMap
的许多操作依赖 CAS 保证线程安全。以下是 CAS 在不同场景中的应用详解:
3.1 初始化表
- 当第一次插入元素时,需要初始化哈希表(
table
)。 - 使用 CAS 确保多个线程不会重复初始化:
流程:if (table == null || table.length == 0) { Node<K,V>[] newTable = new Node[newCapacity]; if (U.compareAndSwapObject(this, TABLE, null, newTable)) { // 初始化成功 } }
- 检查
table
是否为null
。 - 如果为
null
,尝试通过 CAS 将table
设置为新的数组。 - 如果其他线程已经初始化成功,则当前线程的 CAS 操作失败,直接使用已有的
table
。
- 检查
3.2 put 操作
当插入新元素时,ConcurrentHashMap
通过 CAS 保证节点插入的原子性:
(1) 插入新节点
- 如果目标桶(
table[index]
)为空,使用 CAS 将新节点插入:
流程:if (U.compareAndSwapObject(tab, i, null, new Node<K,V>(hash, key, value, null))) { // 插入成功 }
- 判断当前桶是否为空。
- 如果为空,利用 CAS 将节点插入。如果失败,说明有其他线程同时插入,进入下一步逻辑。
3.3 扩容(rehash)
当哈希表需要扩容时,CAS 被用于协调多个线程完成迁移操作:
(1) 设置扩容标志
- 多个线程可能同时检测到需要扩容,只允许一个线程完成初始化新表:
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 当前线程负责扩容 }
(2) 转移节点
- 在扩容过程中,多个线程会协作迁移数据,每次迁移时使用 CAS 确保线程安全。
3.4 get 操作
ConcurrentHashMap
的 get
操作是线程安全的,因为读取操作无需加锁,仅需要保证可见性,使用 volatile
或 CAS。
- 数据读取是无锁的,因为
Node
的链表结构和红黑树的构建本质上是线程安全的。
4. CAS 操作的优缺点在 ConcurrentHashMap 中的体现
优点
- 高并发性能:
- 通过 CAS 避免了全局锁(如
synchronized
或ReentrantLock
),减少了线程阻塞。
- 通过 CAS 避免了全局锁(如
- 局部性优化:
- 操作只针对某个桶(
table[index]
),线程之间竞争更小。
- 操作只针对某个桶(
缺点
- ABA 问题:
- 如果节点的引用发生了变化(例如被移除后又重新插入),可能导致 CAS 操作误判。JDK 的
AtomicStampedReference
提供了解决方案。
- 如果节点的引用发生了变化(例如被移除后又重新插入),可能导致 CAS 操作误判。JDK 的
- 自旋问题:
- 当 CAS 操作失败时,可能需要反复重试,自旋会消耗 CPU 资源。
5. 总结
核心点
-
CAS 的核心作用:
- 保证
ConcurrentHashMap
的线程安全。 - 避免了锁的频繁使用,提升了性能。
- 保证
-
CAS 使用场景:
- 初始化表时,保证单次初始化。
- 插入节点时,确保原子性。
- 扩容时,协作完成迁移。
适用场景
ConcurrentHashMap
适用于高并发读写的场景,例如:
- 缓存存储。
- 计数器或频率统计。
- 并发访问的集合管理。
通过 CAS 与同步块的结合,ConcurrentHashMap
在保证线程安全的同时提供了优秀的性能表现。