1. 什么是CAS
compare and swap,字面意思:比较和交换
寄存器 A 的值 和 内存 M 的值进行对比,如果值相同,就把寄存器B的值和M的值进行交换
// 伪代码
/*
address 内存地址
expectedValue 寄存器A
swapValue 寄存器B
*/
boolean CAS(address, expectValue , swapValue){
if(&address == expectedValue){
&address == swapValue;
return true;
}
return false;
}
上述是伪代码来描述下CAS的操作,但是我们要知道,CAS操作,是一条CPU指令!(是靠的CPU支持)是原子性操作!
2. CAS的应用
2.1 实现原子类
Java标准库中提供了 java.util.concurrent.atomic包,里面的类都是基于这种方式实现的
import java.util.concurrent.atomic.AtomicInteger;
public class Demo27 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
num.getAndIncrement();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
num.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(num.get());
}
}
可以看出结果是对的!
同样使用一段伪代码介绍CAS原理,了解下getIncrement内部实现
// 伪代码
class AtomicInteger{
private int value;
public int getAndIncrement(){
int oldValue = value;
while( CAS(value,oldValue, oldValue+1) != true){
oldValue = value;
}
return oldValue;
}
}
2.2 实现自旋锁
反复检测当前的锁状态,看是否解开了, 来看一段自旋锁伪代码
// 伪代码
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;
}
}
3. CAS中的 ABA 问题
CAS的关键是对比 寄存器和内存中的值,看是否相同~(通过这个对比,来检测,内存是不是改变过)
万一对比的时候是相同的,但是不是没变过,而是从 a -> b -> a,此时就会一定概率出现问题了
举个简单例子:
假设一个老哥有100块的存款,有一天他来从ATM机上取50块钱,假设ATM机有两个线程,并发的执行 - 50 的操作
我们期望的是线程A -50成功,线程B -50失败
如果使用CAS 的方式完成可能会出现问题
正常过程
1) 存款100,线程 A 获取到当前款值为100,期望更新为50;线程B也获取到款值为100,期望更新为50
2)线程A执行扣款成功后,存款变为50,线程B阻塞等待
3)轮到线程B执行,发现存款为50,和之前读到100不同,执行失败
异常的过程
1)存款100,线程 A 获取到当前款值为100,期望更新为50;线程B也获取到款值为100,期望更新为50
2)线程A扣款成功,存款变为50,线程B阻塞中
3)在线程B执行前,突然有个老铁给账户打了50远,此时存款变为100
4) 轮到B执行,此时发现当前存款和之前读的一样,就又执行-50
**这个时候,就出现问题,扣款两次,老哥成了大冤种**~
如何去解决这个ABA问题? ABA关键是值会反复横条~如果约定数据只能单方向变化,问题就迎刃而解了!(只能增加或者只能减少)
此时我们可以引入一个 版本号 变量,约定 版本号 只能增加
我们再一次举上述例子:
假设老哥存款100,要求ATM机上取50,ATM机有两个线程,并发的执行-50操作
我们期望的是一个 -50 成功,一个 -50失败
为了解决上述出现ABA 问题,给余额引入一个版本号,初始为1
1)存款100,线程A获取到 100,版本号为1;线程B也获取到100,版本号为1
2)线程A执行扣款成功后,款值变为50,版本号改为2;线程B阻塞等待中
3)线程B执行操作之前,有个好心人给老公转了50,余额变为100,此时版本号改成3
4)当线程B执行时,发现当前版本号和之前获取版本号不对应,则操作失败