文章目录
这里是@那我掉的头发算什么
刷到我,你的博客算是养成了😁😁😁
CAS
定义与实现
CAS: 全称Compare and swap,字⾯意思:"⽐较并交换",⼀个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- ⽐较 A 与 V 是否相等。(⽐较)
- 如果⽐较相等,将 B 写⼊ V。(交换)
- 返回操作是否成功。
伪代码:

逻辑:
函数参数:address是要操作的内存地址,expectedValue是 "预期内存里现在的值"(对应图里的 "寄存器的值"),swapValue是 "要换成的值"(对应 "另一个寄存器的值")。
操作流程:先判断「内存地址里的实际值」和「预期值expectedValue」是否相等 → 相等的话,就把内存地址里的值换成swapValue,返回true;不相等就直接返回false。
原子性
图里强调 "CAS 是 CPU 的一条指令"------ 这意味着 "比较 + 交换" 这两步操作是原子性的(不会被其他线程打断)。
举个例子:如果两个线程同时对同一个内存地址做 CAS,CPU 会保证只有一个线程能完整完成 "比较 + 交换",另一个线程的 CAS 会因为 "内存值已经被改了" 而失败,这样就避免了多线程的竞态问题。
应用
原子类★

在之前学习的过程中我们遇到过count++线程不安全问题,主要原因是自增操作不是原子的。我们通过加锁强制将这个操作改成原子的,但是加锁就难免效率会低一些。我们就可以通过CAS来实现count++,确保性能也能保证线程安全。只需要将count定义成内部实现了CAS的AtomicInteger即可。
其实不只是++操作,只要是涉及读取一个旧值,基于这个旧值处理,将新值写回内存的操作都会被封装成原子操作,也可以叫"读-改-写"操作。
简单说:只要是 "需要先读当前值,再基于这个值生成新值,最后写回去" 的逻辑,原子类都会把它做成 "不可打断的完整操作"。
比如:
"a = a + 5"(读 a→算 a+5→写 a)→ 原子类用addAndGet(5)封装;
"a = max (a, 10)"(读 a→比大小→写 a)→ 原子类用accumulateAndGet(10, Math::max)封装;
"a = 自定义逻辑 (a)"(读 a→执行自定义函数→写 a)→ 原子类用updateAndGet(自定义函数)封装。
java
package Thread11_21;
import java.util.concurrent.atomic.AtomicInteger;
public class demo36 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
//synchronized (locker){
count.getAndIncrement();
//}
}
System.out.println("t1结束");
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
//synchronized (locker){
count.getAndIncrement();
//}
}
System.out.println("t2结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}

java
// count++;
//count.getAndIncrement();
// ++count;
// count.incrementAndGet();
// count += n;
// count.addAndGet(n);
而此处的getAndIncrement伪代码如下:
在实际业务中,例如此处的getAndIncrement操作,用循环来确保最终的业务操作能完成。

此处的oldValue相当于一个寄存器,存放着某一个线程操作开始时读取到的内存的值,也就是要处理的值。如果这个值被其他的线程修改了,此时CAS的结果会是false,oldValue会重新读取数据,重新操作一次。
自旋锁
CAS还可以用来实现自旋锁:


ABA问题
使用CAS能够保证线程安全的原因是每次在写入数据前,先比较"相等"。本质上是看是否有其他线程插入进来做了一些其他操作使得原数据被改变了。如果数据值没有改变,就认为没有线程插入进来。比如本来判定内存的值是A,再次判定还是A ,就说明没被修改过。
但是!!!有没有一种可能,这个A,是从刚开始的A被修改成了B,然后再修改成A?
比如说,二手的东西被翻新了,那还是新的吗???
其实一般来说,从A又修改成了A,我再操作,似乎也没什么毛病。二手的东西翻新卖,只要他翻新的足够好,让我看不出来是二手的,也没啥毛病。大部分场景下,这种ABA问题即使出现了,也没啥影响。
只有一些极端的场景,ABA问题才会出bug:

比如取钱的时候:
从一千元账户取500元
万一不小心手抖多按几下取款导致出了两个线程来扣款,一个线程修改了原来的值成为500,另一个线程比较时出错,所以不会执行,因而最终不会有差错。
但如果此时另一个线程加了500进去,第二个多出来的线程判断时发现值是1000没错,就会执行扣500操作。
最终是1000 + 500 - 500 = 500,所以出现了线程安全问题。
还有一个更阴的场景:
线程1是加一操作,原数据为x。线程2在线程1 读取完数据后执行了 result =x/x + x-1这样的操作,最后数据没变化,线程1还是加1。但是本来业务的逻辑是想原数据加了1之后再执行 result = (x+1)/x + x - 1,这样的话也会产生偏差。
ABA问题解决方案
上述问题中,使用钱来判别中间是否有线程插入。但是钱可以增加也可以减少,所以引发了上述问题。我们只需要判别时选一个只能增加不能减少的值就可以了。
此时我们就可以引入一个概念:版本号-version
每进行一次操作,版本号都 + 1。
如下:

但是!!!!!!!!版本号的方法只是能帮助你感知到出了问题,比如我说的第二种场景,当然对于取钱的场景是可以完美解决的。像第二种场景感知到了错误之后,后续还是需要由程序员来实际处理问题。
要注意,时间不可以,因为时间会涉及到"闰秒"问题:
闰秒问题本质是「天文时间(地球自转)和原子时间(精确计时)不同步」导致的时间调整,也就是在某一个时间可能全体的操作系统都会将时间向前或者向后调一秒来覆盖偏差。
总结
CAS 是 CPU 原生支持的原子操作,通过 "比较 - 交换" 的原子逻辑实现无锁线程安全,核心用于封装 "读 - 改 - 写" 类 RMW 操作(如原子类的自增、累加,或自旋锁实现),兼顾性能与安全性;其核心缺陷是 ABA 问题,需通过 "值 + 版本号"(如 AtomicStampedReference)双重校验感知数据修改历史,且版本号仅负责 "报警",后续需程序员按业务场景(重试、放弃、调整逻辑)处理,实践中简单 RMW 操作优先用原子类,复杂场景仍需锁机制,非关键场景可忽略 ABA 问题。
