Java CAS原理和用法总结
作者:看透也说透kevin
CAS(Compare And Swap)是一种无锁的原子操作机制,通过一条CPU硬件指令(如x86架构的CMPXCHG指令)实现,能保证操作的原子性,本文给大家介绍java CAS原理和用法,感兴趣的朋友跟随小编一起看看吧
一、CAS 原理
1. 核心思想
CAS 是一种无锁的原子操作机制。它的核心思想是:我认为值应该是A,如果是,那我就把它改成B;如果不是A(说明被别人改过了),那我就不修改,然后可以选择重试或放弃。
这个操作是作为一条CPU硬件指令实现的(在x86架构上是 CMPXCHG 指令),因此它能保证原子性,不会被线程调度打断。
2. 操作模型
CAS 操作涉及三个操作数:
- 内存位置(V)
- 预期的原值(A)
- 新值(B)
伪代码逻辑如下:
if (V == A) {
V = B;
return true;
} else {
return false;
}但关键是,整个比较和交换的过程是一个不可分割的原子操作。
3. 工作流程
当一个线程想要更新一个变量时,它会:
- 获取当前内存中的值,作为期望值
A。 - 计算出新值
B。 - 执行 CAS 指令,判断当前内存中的值是否还是
A。- 如果是,说明没有其他线程修改过,成功将值更新为
B。 - 如果不是,说明值已被其他线程修改,本次更新失败。线程通常会重试整个操作(获取新的当前值,计算新值,再次执行CAS),直到成功为止。这种重试行为就是常见的自旋。
- 如果是,说明没有其他线程修改过,成功将值更新为
4. 优点与缺点
- 优点:
- 高性能:避免了重量级锁(如
synchronized)带来的线程阻塞、唤醒和上下文切换的开销,在竞争不激烈的场景下性能极高。 - 避免死锁:由于是无锁操作,从根本上避免了死锁问题。
- 高性能:避免了重量级锁(如
- 缺点:
- ABA 问题:CAS 只检查值是否变化,但如果一个值从 A 变成 B,又被改回 A,CAS 会误以为它没变。解决方案是使用版本号或标记(如
AtomicStampedReference)。 - 自旋开销:在高竞争环境下,如果线程一直失败重试,会长时间占用 CPU,消耗资源。
- 只能保证一个共享变量的原子操作:对于多个共享变量,CAS 无法保证原子性。但可以将它们合并成一个对象,使用
AtomicReference来保证原子性。
- ABA 问题:CAS 只检查值是否变化,但如果一个值从 A 变成 B,又被改回 A,CAS 会误以为它没变。解决方案是使用版本号或标记(如
二、Java 中的 CAS 用法
在 Java 中,你不能直接使用 CPU 指令。CAS 的能力是通过 sun.misc.Unsafe 类中的本地(Native)方法提供的。但通常,我们不会直接使用 Unsafe,而是使用 JD 在 java.util.concurrent.atomic 包下为我们封装好的原子类。
1. 主要的原子类
- 基本类型:
AtomicInteger:整型原子类AtomicLong:长整型原子类AtomicBoolean:布尔型原子类
- 数组类型:
AtomicIntegerArray:整型数组原子类AtomicLongArray:长整型数组原子类AtomicReferenceArray:引用类型数组原子类
- 引用类型:
AtomicReference:引用类型原子类AtomicMarkableReference:带标记位的引用类型原子类(解决ABA问题的一种方式)AtomicStampedReference:带版本号的引用类型原子类(解决ABA问题的标准方案)
- 字段更新器:
AtomicIntegerFieldUpdater:基于反射,原子性地更新某个类的 volatile int 字段。AtomicLongFieldUpdaterAtomicReferenceFieldUpdater
2. 核心方法
所有原子类都提供了基于 CAS 的核心方法:
boolean compareAndSet(int expect, int update)- 这是最核心的方法!如果当前值等于期望值
expect,则原子地将值设置为update,成功返回true,失败返回false。
- 这是最核心的方法!如果当前值等于期望值
int getAndSet(int newValue)- 原子地设置为新值,并返回旧值。底层通常通过循环 CAS 实现。
int getAndIncrement()/int getAndDecrement()- 原子地递增/递减 1,返回旧值。
i++的原子版本。
- 原子地递增/递减 1,返回旧值。
int getAndAdd(int delta)- 原子地加上 delta,返回旧值。
int incrementAndGet()/int decrementAndGet()- 原子地递增/递减 1,返回新值。
++i的原子版本。
- 原子地递增/递减 1,返回新值。
3. 代码示例
示例 1:使用 AtomicInteger 实现线程安全的计数器
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInt = new AtomicInteger(0);
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
// 内部通过循环CAS操作实现原子递增
atomicInt.incrementAndGet();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// 结果总是 2000,保证了原子性
System.out.println("Final Count: " + atomicInt.get());
}
}示例 2:手动使用 compareAndSet 进行自旋
AtomicInteger atomicInt = new AtomicInteger(0);
int oldValue, newValue;
do {
oldValue = atomicInt.get(); // 获取当前值作为预期值
newValue = oldValue + 1; // 计算新值
} while (!atomicInt.compareAndSet(oldValue, newValue));
// 如果CAS失败(oldValue已不是当前值),则循环重试示例 3:使用 AtomicStampedReference 解决 ABA 问题
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
// 初始值为 100,版本号(Stamp)为 0
static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
int initialStamp = atomicStampedRef.getStamp(); // 获取初始版本号
// 线程1模拟ABA操作
Thread thread1 = new Thread(() -> {
// 先改成 101,版本号+1
atomicStampedRef.compareAndSet(100, 101, initialStamp, initialStamp + 1);
// 再改回 100,版本号再+1
atomicStampedRef.compareAndSet(101, 100, initialStamp + 1, initialStamp + 2);
});
// 线程2尝试修改
Thread thread2 = new Thread(() -> {
// 先睡一会儿,确保线程1完成了ABA操作
try { Thread.sleep(1000); } catch (InterruptedException e) {}
// 尝试修改。虽然期望值还是100,但版本号已经从0变成了2,所以CAS会失败!
boolean success = atomicStampedRef.compareAndSet(
100,
202,
initialStamp, // 传入旧的版本号0
initialStamp + 1
);
System.out.println("CAS successful? " + success); // 输出:false
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}总结
| 特性 | 描述 |
|---|---|
| 本质 | 一条CPU原子指令,通过 Unsafe 类提供给 Java 开发者使用。 |
| 实现 | JDK 的 java.util.concurrent.atomic 包下的原子类对其进行了封装。 |
| 核心方法 | compareAndSet(expectedValue, newValue) |
| 优点 | 无锁、高性能(低竞争时)、避免死锁。 |
| 缺点 | ABA问题(用版本号解决)、自旋CPU开销(高竞争时)。 |
| 应用场景 | 计数器、序列号生成器、ConcurrentHashMap 等高性能并发容器的实现。 |
CAS 是现代并发包(JUC)的基石,理解了它就能更好地理解 ReentrantLock、线程池等高级并发工具的内部工作原理。
到此这篇关于java CAS原理和用法的文章就介绍到这了,更多相关java CAS原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
