大家好 , 这篇文章给大家带来的是多线程当中的 CAS 问题
CAS 是 操作系统 / 硬件 给 JVM 提供的另外一种更轻量的原子操作的机制
推荐大家跳转到 此链接 查看效果更佳~
上一篇文章的链接我也给大家贴到这里了
点击即可跳转到文章专栏~
CAS 问题
CAS 是 操作系统 / 硬件 给 JVM 提供的另外一种更轻量的原子操作的机制
CAS 是 CPU 提供的一个特殊指令 : compare and swap (比较和交换是一条指令 -> 原子的)
compare : 比较内存和寄存器的值 . 如果相等 , 则把寄存器和另一个值进行交换 ; 如果不想等 , 不进行操作
1. CAS 伪代码
// address:内存地址
// expectValue:用来比较的值/预期值(存放在寄存器中)
// swapValue:用来交换的值(存放在另一个寄存器中)
boolean CAS(address, expectValue, swapValue) {
// 拿内存地址对应的值和预期值比较
// 一样进行交换
if (&address == expectedValue) {
// 另一个寄存器的值和内存中的值进行交换
&address = swapValue;
return true;
}
return false;
}
上面的代码 , 我们看着并不是原子的
但是这一系列操作都是由一个 CPU 指令来完成的
2. 典型应用场景
2.1 使用 CAS 实现原子类
原子类是标准库中提供的一组类 , 这个类可以让原子进行 ++ – 等运算
我们之前完成过这个代码
public class Demo27 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count=" + count);
}
}
这个代码得到的值是错误的
这是因为线程不安全导致的问题
我们可以使用 synchronized
关键字解决这个问题
但是我们还有更好的方式
import java.util.concurrent.atomic.AtomicInteger;
public class Demo27 {
// public static int count = 0;
// AtomicInteger:原子类整数
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// 相当于count++;
count.getAndIncrement();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// 相当于++count
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count=" + count);
}
}
这个原子类是怎样实现的呢 ?
来看这段伪代码
class AtomicInteger {
private int value;
public int getAndIncrement() {
// 先把旧的 value 值存储下来
// oldValue 就相当于寄存器(因为在代码中没有直接的寄存器)
int oldValue = value;
// 判断新读取到的 value 值(内存中的值)和旧的 value 值(寄存器中的值)是不是一样的
// 一样的就把寄存器(oldValue)的值+1,再和内存中的值进行交换
// 不一样的话就进入循环,重新读取 value 的值
while ( CAS(value, oldValue, oldValue + 1) != true) {
oldValue = value;
}
return oldValue;
}
}
2.2 使用 CAS 实现自旋锁
先来看伪代码
public class SpinLock {
// 表示当前是由谁来加锁
private Thread owner = null;
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
public void lock(){
// 判断当前 owner 是否为空
// 为空:代表当前没加锁,那就要进行交换,把当前这里要去加锁的值赋值到 owner 里面了
// 不为空:说明当前的锁被其他线程占用了, CAS 就返回 false,循环继续进行,就变成了自旋的状态;
// 直到 owner 的值被设置成 null 了,我们 CAS 才能完成交换,退出循环
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
3. CAS 的 ABA 问题
举个栗子 :
我们作为一个普通的老百姓 , 在网上买个手机 , 我们正常是无法区分这是一台新机还是一台翻新机
类似的 , 在 CAS 中 , 也无法区分 , 数据始终就是 A , 还是从 A -> B -> A , 后面的这种情况 , 就很有可能出现问题
滑稽老哥有 1000 块钱存款 , 他想从 ATM 中取走 500
我们假设 ATM 按照 CAS 的方式来进行操作
滑稽老哥取钱的时候 , 按下取款按钮 , 就会触发一个 “取钱线程” , 但是滑稽手一滑 , 连续按了两下 , 就产生了两个线程
线程 3 这种情况 , 是极端情况下的小概率事件
没有合适时机切入的线程 3 , 也就不存在正好把值改成原来的值
那么 ABA 一般也不会有 bug
针对 ABA 的解决 , 有很多种办法
正经的解决 ABA 问题的办法 , 是想办法获取到中间的过程
通过引入 “版本号” 来解决这个问题
上面的 CAS , 是比较余额 , 余额相等就可以修改 , 但是余额是可以变大也可以变小 , 因此就有可能出现 ABA 问题
如果换成版本号 , 约定版本号只能增不能减 , 就可以避免 ABA 问题
4. 相关面试题
- 讲解下你自己理解的 CAS 机制
全称 Compare and swap , 即 “比较并交换”. 相当于通过一个原子的操作 , 同时完成 “读取内存 , 比
较是否相等 , 修改内存” 这三个步骤 . 本质上需要 CPU 指令的支撑 .
- ABA 问题怎么解决 ?
给要修改的数据引入版本号 . 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期 .
如果发现当前版本号和之前读到的版本号一致 , 就真正执行修改操作 , 并让版本号自增 ; 如果发现当前版本号比之前读到的版本号大 , 就认为操作失败 .
CAS 问题就给大家讲解完毕
有帮助的话请一键三连~