一、什么是CAS?
CAS操作是一条CPU指令
CAS—>Compare and swap—>比较并交换
比较并交换什么呢?
举个栗子:寄存器A的值和内存M的值进行对比,如果相同,就把寄存器B的值和内存M的值进行交换~
下面通过伪代码说明:
注意:!真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解 CAS 的工作流程
//address--->内存地址、expectValue--->寄存器A、swapValue--->寄存器B
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
我们可以轻易的发现,上述代码是非原子的,在运行过程中可能就会因为线程调度而出现问题
但我们的CSA操作则是原子的,正如上面说的—>CAS操作是一条CPU指令
CAS让我们不加锁就把保证了线程安全~
二、CAS的应用
基于CAS我们可以进行很多操作:
2.1 实现原子类
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于CAS的方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作
下面用代码演示AtomicInteger 类(能够保证+±-时线程安全)的使用:
import java.util.concurrent.atomic.AtomicInteger;
public class Thread_Test27 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
num.getAndIncrement(); //相当于num++
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
num.getAndIncrement();
}
});
t1.start();
t2.start();
//主线程阻塞等待t1和t2
t1.join();
t2.join();
//get()获取数值
System.out.println(num.get());
}
}
结果:
我们可以看到t1和t2没有互相影响,这都得益于CAS~
下面来看伪代码的实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value; //这里的oldValue可以视为寄存器,因为Java没法表示寄存器中的值,所以此处用变量表示
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
上述while循环:
先判断value和oldvalue值是否相等,如果相等就把‘oldvalue+1’的值设置到value中(相当于++),CAS返回true,循环结束~
如果值不相等,CAS返回false,进入循环,重新设置oldvalue的值!
注意:假如在多线程情况下,线程t1已经执行完CAS,此时t2也调用getAndIncrement()方法
上面这样的情况是绝对会出现的!!但是由于CAS时原子的,所以不会影响线程安全,此时会进行下一次循环,而此时已经执行了oldvalue+1,所以和value相等了,继续往下执行~~
在此处的CAS就是在确认当前value值是不是变过,没变过才能自增,要是变过就更新值后再自增!!!
之前写到的线程不安全的原因就是两个线程之间不能感知到对方有没有对数值进行修改!!
2.2 实现自旋锁(Spin lock)
自旋锁(Spin lock)的特点就是会反复查看当前锁的状态,看是否解开了
下面看实现自旋锁的伪代码:
public class SpinLock {
private Thread owner = null; //记录当前锁哪个线程持有,null表示没有线程持有锁
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
分析这里的 while(!CAS(this.owner, null, Thread.currentThread()))
---->
如果当前owner为null,则比较成功,将当前线程的引用(Thread.currentThread()
)设置到owner中,加锁完成,循环结束
如果owner不为空,则比较失败,锁已经有线程持有了,此时CAS啥也不干,直接返回false,循环继续,此时这个循环就会转得飞快,不断确定当前锁是否被释放了~
好处:一旦锁释放就能立刻获取
坏处;产生忙等
三、CAS的ABA问题
3.1 什么是ABA问题?
CAS关键是对比内存和寄存器的值,看看是否相同,通过这样的对比来检测内存是不是改变过。可万一对比的时候是相同的,但不是没变过,而是A–>B–>A,这样的情况下就有一定的概率出问题~
这便是CAS的ABA问题
3.2 ABA问题引发的bug
假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作. 我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
如果使用 CAS 的方式来完成这个扣款过程就可能出现问题:
这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼!!
3.3 如何解决ABA问题?
我们可以知道的是ABA问题的关键就是值会反复横跳,如果我们约定数据只能单方向变化(只能增大或只能减小),问题就迎刃而解~~
如果需求要求该数据既要能增大也要能减小怎么办?—>引入新变量:版本号
约定版本号只能增加(每次修改值都会让版本号增加)
这样的话每次CAS对比就不是对比数据本身,而是对比版本号
下面用伪代码帮助理解:
num = 100;
version = 1;
old = num;
CAS(num,old,old+1,version);