参考:
破解Java CAS操作:解锁无锁编程的奥秘与陷阱 - 云原生实践
Java 魔法类 Unsafe 详解 | JavaGuide
一. CAS基本概念
CAS(Compare-And-Swap)是一种无锁编程的核心技术,用于实现多线程环境下的原子操作。其核心思想是"先比较,再交换",具体操作包含三个参数:
- 内存位置(变量V)
- 预期原值(A)
- 新值(B)
当且仅当内存位置V的值等于预期值A时,才会将V的值更新为B,否则不执行操作。整个过程是原子的,无需加锁即可保证线程安全。
二. Unsafe
类
在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是Unsafe
。
为什么需要Unsafe?
Unsafe
是位于 sun.misc
包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe
类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe
类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再"安全",因此对 Unsafe
的使用一定要慎重。
Unsafe
类中的 CAS 方法是native
方法。native
关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java 语言并没有直接用 Java 实现 CAS。
三. Java中实现CAS操作
3.1 Java实现
Java中的Unsafe
类提供了底层操作内存、线程通信等功能,它是实现CAS机制的基础。Unsafe
类中的compareAndSwapObject,compareAndSwapInt,compareAndSwapLong
方法可以用来实现CAS操作:
java
/**
* CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
这个方法需要传入对象、偏移量、期望值和新值,如果对象的偏移量处的值等于期望值,则将其更新为新值,并返回true
;否则,不做任何操作,并返回false
。
CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数------内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe
提供的 CAS 方法(如 compareAndSwapXXX
)底层实现即为 CPU 指令 cmpxchg
。
3.2 典型应用
在 JUC 包的并发工具类中大量地使用了 CAS 操作。在 Unsafe
类中,提供了compareAndSwapObject
、compareAndSwapInt
、compareAndSwapLong
方法来实现的对Object
、int
、long
类型的 CAS 操作。
csharp
private volatile int a = 0; // 共享变量,初始值为 0
private static final Unsafe unsafe;
private static final long fieldOffset;
static {
try {
// 获取 Unsafe 实例
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
// 获取 a 字段的内存偏移量
fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
} catch (Exception e) {
throw new RuntimeException("Failed to initialize Unsafe or field offset", e);
}
}
public static void main(String[] args) {
CasTest casTest = new CasTest();
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 4; i++) {
casTest.incrementAndPrint(i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 5; i <= 9; i++) {
casTest.incrementAndPrint(i);
}
});
t1.start();
t2.start();
// 等待线程结束,以便观察完整输出 (可选,用于演示)
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 将递增和打印操作封装在一个原子性更强的方法内
private void incrementAndPrint(int targetValue) {
while (true) {
int currentValue = a; // 读取当前 a 的值
// 只有当 a 的当前值等于目标值的前一个值时,才尝试更新
if (currentValue == targetValue - 1) {
if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) {
// CAS 成功,说明成功将 a 更新为 targetValue
System.out.print(targetValue + " ");
break; // 成功更新并打印后退出循环
}
// 如果 CAS 失败,意味着在读取 currentValue 和执行 CAS 之间,a 的值被其他线程修改了,
// 此时 currentValue 已经不是 a 的最新值,需要重新读取并重试。
}
// 如果 currentValue != targetValue - 1,说明还没轮到当前线程更新,
// 或者已经被其他线程更新超过了,让出CPU给其他线程机会。
// 对于严格顺序递增的场景,如果 current > targetValue - 1,可能意味着逻辑错误或死循环,
// 但在此示例中,我们期望线程能按顺序执行。
Thread.yield(); // 提示CPU调度器可以切换线程,减少无效自旋
}
}
需要注意的是:
- 自旋逻辑:
compareAndSwapInt
方法本身只执行一次比较和交换操作,并立即返回结果。因此,为了确保操作最终成功(在值符合预期的情况下),我们需要在代码中显式地实现自旋逻辑(如while(true)
循环),不断尝试直到 CAS 操作成功。 AtomicInteger
的实现: JDK 中的java.util.concurrent.atomic.AtomicInteger
类内部正是利用了类似的 CAS 操作和自旋逻辑来实现其原子性的getAndIncrement()
,compareAndSet()
等方法。直接使用AtomicInteger
通常是更安全、更推荐的做法,因为它封装了底层的复杂性。- ABA 问题: CAS 操作本身存在 ABA 问题(一个值从 A 变为 B,再变回 A,CAS 检查时会认为值没有变过)。在某些场景下,如果值的变化历史很重要,可能需要使用
AtomicStampedReference
来解决。但在本例的简单递增场景中,ABA 问题通常不构成影响。 - CPU 消耗: 长时间的自旋会消耗 CPU 资源。在竞争激烈或条件长时间不满足的情况下,可以考虑加入更复杂的退避策略(如
Thread.sleep()
或LockSupport.parkNanos()
)来优化。
3.3 CAS的存在问题
1.ABA问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。
2. 循环时间开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能够支持处理器提供的pause
指令,那么自旋操作的效率将有所提升。pause
指令有两个重要作用:
- 延迟流水线执行指令:
pause
指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。 - 避免内存顺序冲突:在退出循环时,
pause
指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。
3. 只能保证一个共享变量的原子操作
CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了AtomicReference
类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用AtomicReference
来执行 CAS 操作。