Javaee—— CAS - 详解
一、什么是CAS
CAS:全称Compareandswap,字面意思:“比较并交换”,一个CAS涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
1)比较A与V是否相等。(比较)
2)如果比较相等,将B写入V。(交换)
3)返回操作是否成功。
1、CAS伪代码
下面写的代码不是原子的,真实的CAS是一个原子的硬件指令完成的,这个伪代码只是辅助理解CAS 的工作流程。
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
两种典型的不是"原子性"的代码
1)checkandset(if 判定然后设定值)[上面的CAS伪代码就是这种形式]
2)readandupdate(i++)[之前我们讲线程安全的代码例子是这种形式]
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。
CAS可以视为是一种乐观锁。(或者可以理解成CAS是乐观锁的以种实现方式)
二、CAS是怎么实现的
针对不同的操作系统,JVM用到了不同的CAS实现原理,简单来讲:
- java的CAS利用的的是unsafe这个类提供的CAS操作;
- unsafe的CAS依赖了的是jvm针对不同的操作系统实现的Atomic::cmpxchg;
- Atomic::cmpxchg的实现使用了汇编的CAS操作,并使用cpu硬件提供的lock机制保证其原子 性。
简而言之,是因为硬件予以了支持,软件层面才能做到。
三、CAS有哪些应用
1、实现原子类
标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的,典型的就是AtomicInteger类,其中的getAndIncrement相当于i++操作。
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
伪代码实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
假设两个线程同时调用getAndIncrement
1)两个线程都读取value的值到oldValue中。(oldValue是一个局部变量,在栈上,每个线程有自己的栈)

2)线程1先执行CAS操作,由于oldValue和value的值相同,直接进行对value赋值。
#注:
CAS是直接读写内存的,而不是操作寄存器。
CAS的读内存,比较,写内存操作是一条硬件指令,是原子的。

3)线程2再执行CAS操作,第一次CAS的时候发现oldValue和value不相等,不能进行赋值,因此需要进入循环。
在循环里重新读取value的值赋给oldValue
4)线程2接下来第⼆次执行CAS,此时oldValue和value相同,于是直接执行赋值操作。

5)线程1和线程2返回各自的oldValue的值即可。
通过形如上述代码就可以实现一个原子类,不需要使用重量级锁,就可以高效的完成多线程的自增操作。
本来checkandset这样的操作在代码角度不是原子的,但是在硬件层面上可以让一条指令完成这个操作,也就变成原子的了。
2、实现自旋锁
基于CAS实现更灵活的锁,获取到更多的控制权。
自旋锁伪代码
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
四、CAS的ABA问题

这里检测到他的预期值与实际值不一样,不会赋值,就会自旋
1、什么是ABA问题
ABA的问题:
假设存在两个线程t1和t2,有一个共享变量num,初始值为A。
接下来,线程t1想使用CAS把num值改成Z,那么就需要
先读取num的值,记录到oldNum变量中。
使用CAS判定当前num的值是否为A,如果为A,就修改成Z。
但是,在t1执行这两个操作之间,t2线程可能把num的值从A改成了B,又从B改成了A
线程t1的CAS是期望num不变就修改,但是num的值已经被t2给改了,只不过又改成A了,这个时候t1究竟是否要更新num的值为Z呢?
到这一步,t1线程无法区分当前这个变量始终是A,还是经历了一个变化过程。

这就好比,我们买一个手机,无法判定这个手机是刚出厂的新手机,还是别人用旧了,又翻新过的手机。
2、ABA问题引来的BUG
大部分的情况下,t2线程这样的一个反复横跳改动,对于t1是否修改num是没有影响的,但是不排除一些特殊情况。
假设滑稽老哥有100存款,滑稽想从ATM取50块钱,取款机创建了两个线程,并发的来执行-50操作。
我们期望一个线程执行-50成功,另一个线程-50失败。
如果使用CAS的方式来完成这个扣款过程就可能出现问题。
正常的过程:
1)存款100,线程1获取到当前存款值为100,期望更新为50;线程2获取到当前存款值为100,期望更新为50。
2)线程1执行扣款成功,存款被改成50,线程2阻塞等待中。
3)轮到线程2执行了,发现当前存款为50,和之前读到的100不相同,执行失败。
异常的过程:
1)存款100,线程1获取到当前存款值为100,期望更新为50;线程2获取到当前存款值为100,期望更新为50。
2)线程1执行扣款成功,存款被改成50,线程2阻塞等待中。
3)在线程2执行之前,滑稽的朋友正好给滑稽转账50,账户余额变成100!!
4)轮到线程2执行了,发现当前存款为100,和之前读到的100相同,再次执行扣款操作。
这个时候,扣款操作被执行了两次!!!都是ABA问题搞的贵!!!
3、解决方案
给要修改的值,引入版本号,在CAS比较数据当前值和旧值的同时,也要比较版本号是否符合预期
CAS操作在读取旧值的同时,也要读取版本号。
真正修改的时候:
如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1。
如果当前版本号高于读到的版本号,就操作失败(认为数据已经被修改过了)。
这就好比,判定这个手机是否是翻新机,那么就需要收集每个手机的数据,第一次挂在电商网站上的手机记为版本1,以后每次这个手机出现在电商网站上,就把版本号进行递增。这样如果买家不在意这是翻新机,就买,如果买家在意,就可以直接略过。
对比理解上面的转账例子
假设滑稽老哥有100存款,滑稽想从ATM取50块钱,取款机创建了两个线程,并发的来执行-50操作。
我们期望一个线程执行-50成功,另一个线程-50失败。
为了解决ABA问题,给余额搭配一个版本号,初始设为1。
1)存款100,线程1获取到存款值为100,版本号为1,期望更新为50;线程2获取到存款值为100,版本号为1,期望更新为50。
2)线程1执行扣款成功,存款被改成50,版本号改为2,线程2阻塞等待中。
3)在线程2执行之前,滑稽的朋友正好给滑稽转账50,账户余额变成100,版本号变成3。
4)轮到线程2执行了,发现当前存款为100,和之前读到的100相同,但是当前版本号为3,之前读到的版本号为1,版本小于当前版本,认为操作失败。
在Java标准库中提供了 AtomicStampedReference 类,这个类可以对某个类进行包装,在内部就提供了上面描述的版本管理功能。
关于 AtomicStampedReference 的具体⽤法此处不再展开,有需要的同学自行查找文档了解使用方法即可
3.那有什么解决方案?
在 CAS 机制中引入版本号,主要是为了解决ABA 问题:
什么是 ABA 问题?
假设变量值从A被线程 1 改成B,又被线程 2 改回A,此时线程 3 执行 CAS 操作时,会误认为变量从未被修改过(因为预期值A与内存值A相等),但实际上变量已经被修改过两次。这种场景下,CAS 的 “比较 - 交换” 会出现逻辑漏洞。
引入版本号的解决思路
给变量增加一个版本号(或时间戳),每次变量值被修改时,版本号也同步递增。这样,即使变量值从A变回A,版本号也会从n变为n+2,CAS 操作时需同时比较变量值和版本号,确保两者都符合预期才执行更新。
典型实现:AtomicStampedReference(Java)
java.util.concurrent.atomic.AtomicStampedReference是 Java 中解决 ABA 问题的原子类,核心逻辑如下:
// 初始化:值为"初始值",版本号为0
AtomicStampedReference asr = new AtomicStampedReference<>("初始值", 0);
// 尝试更新:预期值为"初始值",预期版本号为0;更新为"新值",版本号+1
boolean success = asr.compareAndSet(
"初始值", // 预期值A
"新值", // 新值B
0, // 预期版本号
1 // 新版本号
);
- 只有当变量当前值等于预期值且当前版本号等于预期版本号时,才会执行更新,同时版本号递增。
- 若变量值被其他线程修改(即使改回原值),版本号也会变化,从而避免 ABA 问题。
应用场景
适用于变量值可能被反复修改回原值的并发场景,例如:
- 并发栈的弹出操作(防止栈顶元素被反复修改后误判)。
- 分布式缓存的原子更新(确保缓存值的修改是基于最新版本)。
特点
原子性通过 CPU 原子指令(如 x86 架构的
cmpxchg指令)保证 “读取 - 比较 - 更新” 三步操作的原子性,无需加锁即可避免多线程并发修改导致的数据错乱。非阻塞性属于乐观锁思想,不主动阻塞线程:若操作失败(变量被其他线程修改),线程可以选择重试或放弃,而非被挂起,在竞争不激烈时性能优于
synchronized等悲观锁。高效性避免了悲观锁的线程上下文切换和阻塞开销,适合轻量级、高并发的场景(如计数器、原子类操作)。
局限性
- ABA 问题:变量值从
A被改为B再改回A时,CAS 会误判为未修改(可通过绑定版本号解决,如AtomicStampedReference)。 - 自旋开销:若竞争激烈,线程会反复重试 CAS 操作,导致 CPU 资源浪费。
- 单变量原子性:仅能保证单个共享变量的原子操作,无法直接处理多个变量的复合操作。
- ABA 问题:变量值从
线程上下文切换(Thread Context Switch)是多线程环境中,CPU 从一个线程切换到另一个线程执行时,保存当前线程状态并加载目标线程状态的过程,是操作系统实现多任务并发的核心机制。
核心逻辑
- 线程状态:每个线程执行时,CPU 寄存器(如程序计数器、栈指针等)会保存其当前执行位置、局部变量等关键信息(即 “上下文”)。
- 切换过程:当操作系统调度器决定切换线程时(如时间片用完、线程阻塞等),会先将当前线程的上下文保存到内存(如线程控制块 TCB),再从内存中加载目标线程的上下文到 CPU 寄存器,最终 CPU 开始执行目标线程。
触发场景
- 时间片轮转:操作系统为每个线程分配时间片(如 10ms),时间片用完后强制切换到其他线程。
- 线程阻塞:线程执行 I/O 操作(如读写文件、网络请求)、等待锁或 sleep () 时,主动放弃 CPU,触发切换。
- 优先级调度:高优先级线程就绪时,抢占低优先级线程的 CPU 资源,导致切换。
性能影响
- 开销来源:上下文切换需要读写内存(保存 / 加载上下文),会消耗 CPU 时间,频繁切换会显著降低程序性能(尤其在高并发场景)。
- 优化方向:减少不必要的线程创建(控制线程数量)、避免线程频繁阻塞(如使用非阻塞 I/O)、减少锁竞争等,可降低切换频率。
与进程切换的区别
- 线程上下文切换:同一进程内的线程共享内存空间,切换时只需保存线程私有数据(寄存器、栈等),开销较小。
- 进程上下文切换:不同进程的内存空间独立,切换时需额外切换页表(内存映射),开销是线程切换的几十倍甚至更多。
浙公网安备 33010602011771号