了解CAS与使用
了解CAS与使用
一、CAS 介绍
1.1 什么是 CAS
-
定义:CAS(Compare And Swap,比较与交换)是 CPU 硬件层面的原子指令,是无锁同步的实现原理,可看作乐观锁的一种实现方式。
-
核心逻辑:操作包含三个参数 ——内存值 V(目标变量的内存地址值)、预期值 E(线程认为变量应有的值)、新值 N(线程要更新的新值);执行时,当且仅当 V == E 时,才将 V 更新为 N,无论是否更新,都会返回旧的内存值 V,整个过程是原子操作(硬件层面保障)。
-
Java 伪代码模拟:
public synchronized int compareAndSwap(int expectedValue, int newValue) { int oldRamAddress = accessMemory(ramAddress); // 读取内存值V if (oldRamAddress == expectedValue) { // 比较V与E ramAddress = newValue; // 相等则更新为N } return oldRamAddress; // 返回旧值V }
1.2 CAS 的 Java 支持(Unsafe 类)
-
Unsafe 类角色:位于
sun.misc包,提供低级别、不安全操作(如直接访问内存),是 CAS 操作的底层支持,其 CAS 方法为 native 方法(由 JVM 实现,不同虚拟机可能有差异)。 -
核心 CAS 方法:
方法声明 作用 compareAndSwapObject(Object var1, long var2, Object var4, Object var5)原子更新对象类型变量 compareAndSwapInt(Object var1, long var2, int var4, int var5)原子更新 int 类型变量 compareAndSwapLong(Object var1, long var2, long var4, long var6)原子更新 long 类型变量 -
参数说明(以
compareAndSwapInt为例):var1:对象实例(要更新的变量所属对象)var2:内存偏移量(变量在对象中的内存地址偏移,通过unsafe.objectFieldOffset()获取)var4:字段期望值 Evar5:字段新值 N
1.3 CAS 的应用场景
CAS 在 Java 并发组件中应用广泛,核心场景包括:
java.util.concurrent.atomic包下的原子类(如 AtomicInteger)- Java AQS(AbstractQueuedSynchronizer,抽象队列同步器)
- CurrentHashMap(并发哈希表)
1.4 CAS 的源码分析(Hotspot 虚拟机)
1.4.1 核心流程
-
Unsafe_CompareAndSwapInt 方法:接收对象、偏移量、期望值、新值,解析对象地址,计算目标变量的内存地址,调用
Atomic::cmpxchg执行 CAS 逻辑。UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // 计算变量内存地址 return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 执行CAS并判断结果 UNSAFE_END -
Atomic::cmpxchg 方法(以 Linux x86 为例):依赖 CPU 指令
cmpxchgl,多处理器环境下添加lock前缀保证内存屏障,实现 “比较 - 交换” 原子性。- 指令逻辑:比较
dest(内存地址)的值与compare_value(E),相等则将exchange_value(N)写入dest,否则将dest值写入exchange_value。
- 指令逻辑:比较
1.5 CAS 的缺陷
| 缺陷类型 | 具体说明 |
|---|---|
| 自旋开销大 | 若 CAS 长时间失败(如高并发下),线程会持续自旋重试,占用大量 CPU 资源 |
| 仅支持单个共享变量 | CAS 只能保证单个变量更新的原子性,无法直接实现多个变量的原子操作(需组合成对象间接实现) |
| ABA 问题 | 线程 1 读取值为 A,线程 2 将 A→B 再→A,线程 1 再次 CAS 时,认为值未修改而更新成功,忽略中间变更 |
1.6 ABA 问题及解决方案
1.6.1 什么是 ABA 问题
- 场景:多线程操作原子类时,某线程短时间内将值从 A 改为 B,再改回 A,其他线程无法感知中间变更,导致 CAS 误判成功。
- 代码示例:AtomicInteger 初始值为 1,Thread2 将 1→2→1,Thread1 阻塞后 CAS 1→3 仍成功,误以为值未修改。
1.6.2 解决方案
- AtomicStampedReference:
- 原理:维护 “引用值(reference)+ 版本号(stamp)”,每次修改时不仅更新值,还将版本号 +1,CAS 时需同时比较 “值” 和 “版本号”,确保中间无变更。
- 核心方法:
compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
- AtomicMarkableReference:
- 原理:简化版 AtomicStampedReference,用 boolean 类型的
mark标记值是否被修改(不关心修改次数,仅关心 “是否修改过”)。
- 原理:简化版 AtomicStampedReference,用 boolean 类型的
二、Atomic 原子操作类介绍
2.1 原子类概述
- 核心作用:在并发编程中,以乐观锁(CAS)实现线程安全的变量更新,替代悲观锁(synchronized),提升性能。
- 分类:共 5 大类,覆盖基本类型、引用类型、数组、对象属性、累加场景,具体如下表:
| 分类 | 具体类名 | 核心用途 |
|---|---|---|
| 基本类型 | AtomicInteger、AtomicLong、AtomicBoolean | 原子更新 int、long、boolean 类型变量 |
| 引用类型 | AtomicReference、AtomicStampedReference、AtomicMarkableReference | 原子更新对象引用,解决 ABA 问题 |
| 数组类型 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray | 按索引原子更新数组中的元素 |
| 对象属性修改器 | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater | 原子更新对象的实例字段(无需修改字段所属类) |
| 原子累加器(JDK1.8+) | LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator | 高并发场景下高效累加,性能优于传统原子类 |
2.2 各类原子类详解
2.2.1 原子更新基本类型(以 AtomicInteger 为例)
-
核心方法:
方法声明 描述 返回值 getAndIncrement()原子自增 1,先返回旧值 自增前的旧值 incrementAndGet()原子自增 1,先自增再返回新值 自增后的新值 getAndSet(int newValue)原子更新为新值,返回旧值 更新前的旧值 addAndGet(int delta)原子累加 delta,返回新值 累加后的新值 -
实现原理:依赖 Unsafe 的
getAndAddInt方法,通过自旋 CAS 重试直到更新成功。 -
测试示例:10 个线程各自增 10000 次,最终 sum 为 100000(线程安全)。
2.2.2 原子更新数组类型(以 AtomicIntegerArray 为例)
-
核心方法:
方法声明 描述 addAndGet(int i, int delta)原子更新数组索引 i 的元素,累加 delta 后返回新值 getAndIncrement(int i)原子更新数组索引 i 的元素,自增 1 后返回旧值 compareAndSet(int i, int expect, int update)原子更新数组索引 i 的元素,预期值为 expect 则更新为 update -
特点:操作直接作用于数组元素,需传入数组索引,保证数组元素更新的线程安全。
2.2.3 原子更新引用类型(以 AtomicReference 为例)
-
核心作用:封装普通对象引用,保证对象引用更新的线程安全(区别于基本类型,可操作自定义对象)。
-
代码示例:
AtomicReference<User> atomicRef = new AtomicReference<>(user1); atomicRef.compareAndSet(user1, user2); // 原子将引用从user1改为user2 -
扩展:AtomicStampedReference 和 AtomicMarkableReference 用于解决引用更新的 ABA 问题。
2.2.4 原子更新对象属性(以 AtomicIntegerFieldUpdater 为例)
- 核心作用:无需修改对象类的源码,通过反射原子更新对象的整型实例字段。
- 使用约束(共 5 点):
- 字段必须是 volatile 类型(保证线程间可见性);
- 字段访问权限与调用者匹配(如调用者可访问私有字段才能原子更新);
- 字段必须是 实例变量(不能加 static 关键字);
- 字段不能是 final 类型(final 不可修改,与原子更新冲突);
- 仅支持 int/long 基本类型(包装类型需用 AtomicReferenceFieldUpdater)。
2.2.5 原子累加器(以 LongAdder 为例)
-
设计背景:高并发下,AtomicLong 的自旋 CAS 会因线程冲突频繁导致性能瓶颈,LongAdder 通过 “分散热点” 提升性能。
-
核心原理:
- 结构:继承 Striped64,包含
base(非竞态时的基数)和Cell[]数组(竞态时,线程将值累加到各自的 Cell 槽中); - 逻辑:无并发冲突时,直接累加
base;有冲突时,线程通过哈希映射到Cell[]的某一槽,仅对该槽的 value 执行 CAS 操作,分散热点。
- 结构:继承 Striped64,包含
-
核心方法:
方法声明 描述 特点 add(long x)原子累加 x 高并发时分散到 Cell 槽,冲突少 sum()计算总累加值(base + 所有 Cell 的 value) 非原子快照,高并发下返回近似值 -
性能优势:线程数越多、并发操作次数越大,LongAdder 优势越明显(对比 AtomicLong),适用于 写多 read 少 的场景;低并发场景下,AtomicLong 足够高效。
关键问题
问题 1:CAS 能保证原子性的根本原因是什么?它与 synchronized 实现原子性的思路有何本质区别?
答案:
- CAS 原子性的根本原因:CAS 是 CPU 硬件层面的原子指令(如 x86 架构的
cmpxchgl指令),硬件直接保障 “比较 - 交换” 两个操作的不可分割性,无需软件层面的锁机制;即使在多处理器环境下,也可通过lock前缀指令实现内存屏障,防止指令重排序和多核心缓存不一致,进一步保证原子性。 - 与 synchronized 的本质区别:
- 锁策略:synchronized 是 悲观锁,默认认为线程会冲突,通过阻塞线程(获取锁→执行→释放锁)保证原子性,本质是 “以时间换空间”;CAS 是 乐观锁,默认认为线程冲突概率低,通过自旋重试(不阻塞线程)实现无锁同步,本质是 “以空间换时间”;
- 线程状态:synchronized 会导致线程阻塞 / 唤醒(内核态切换),开销大;CAS 线程始终处于运行态(用户态),仅自旋重试时占用 CPU,无内核态切换开销;
- 适用场景:synchronized 适用于冲突频繁、执行时间长的场景;CAS 适用于冲突少、执行时间短的场景(如原子类更新)。
问题 2:LongAdder 为何能在高并发场景下比 AtomicLong 性能更优?它的设计思路和局限性是什么?
答案:
- 高并发性能优的核心原因:AtomicLong 所有线程竞争同一个热点变量
value,高并发下 CAS 冲突频繁,大量线程自旋重试导致 CPU 开销大;而 LongAdder 通过 “分散热点” 设计,将单个热点变量拆分为 “base 基数 + Cell [] 数组”,具体逻辑如下:- 无冲突时:线程直接累加
base,与 AtomicLong 性能相当; - 有冲突时:线程通过哈希算法映射到
Cell[]数组的某一槽(Cell),仅对该槽的value执行 CAS 操作,不同线程操作不同 Cell,大幅减少冲突概率,提升并发效率。
- 无冲突时:线程直接累加
- 设计思路:基于 Striped64 抽象类,核心是 “将竞争分散到多个存储单元,降低单个单元的竞争强度”,本质是 “空间换时间” 的优化。
- 局限性:
- 求和非原子:
sum()方法计算总计时,需遍历Cell[]数组累加base和所有 Cell 的value,此过程无锁,若其他线程同时修改 Cell,sum()返回的是 “近似值”,非调用时刻的原子快照; - 功能单一:仅支持累加 / 递减操作,无法实现 AtomicLong 的
getAndSet、compareAndSet等复杂原子操作; - 内存开销大:
Cell[]数组需占用额外内存(数组大小为 CPU 核数的 2 次幂),低并发场景下内存开销高于 AtomicLong。
- 求和非原子:
问题 3:ABA 问题会导致什么业务风险?除了文档中的 AtomicStampedReference,还有其他解决方案吗?
答案:
- ABA 问题的业务风险:若业务场景需感知变量的 “中间变更”,ABA 问题会导致逻辑错误,例如:
- 资金转账场景:用户 A 账户余额 100 元(A),线程 1 读取余额准备扣减 50 元,线程 2 先将 100→0(B,转账给 B)再→100(A,B 退款),线程 1 CAS 100→50 成功,但实际账户曾被清空,可能违反 “余额不足不允许扣减” 的业务规则;
- 链表节点操作场景:线程 1 准备删除链表节点 A,线程 2 先删除 A 再插入新节点 A,线程 1 CAS 时误判节点未变,导致链表结构损坏。
- 其他解决方案:
- 版本号机制(自定义实现):在变量中额外维护一个版本号字段(如
volatile long version),每次修改变量时版本号 +1,CAS 时同时比较 “变量值” 和 “版本号”,确保版本号连续递增(类似 AtomicStampedReference 的底层逻辑); - 时间戳机制:用时间戳替代版本号,每次修改时记录当前时间戳,CAS 时比较 “值 + 时间戳”,若时间戳变化则说明中间有修改;
- 不可变对象:将变量设计为不可变对象(如 String),修改时创建新对象而非修改原对象,此时变量引用的变更不会出现 “旧值复用”,从根本上避免 ABA 问题(但会增加对象创建开销,适用于修改频率低的场景)。
- 版本号机制(自定义实现):在变量中额外维护一个版本号字段(如

浙公网安备 33010602011771号