一、什么是 CAS
CAS(Compare and swap),即“比较并交换”。它就是计算机编程中的原子操作,没错,就是“比较 + 交换”的 原子操作。
对于一个 CAS 操作:比较内存中的一个值和给定的值,如果相同,就把内存中的值替换为新的值。比如,有 A、B、C三个数,A 为内存中的值,B 为预期的值,C 为要赋给 A 的值。将会有下面的三个步骤:
- 比较 A 与 B 是否相等。
- 如果 A 与 B 相等,则 A = C(交换)。
- 返回一个代表“是否成功”的值。
二、CAS 实现的伪代码
CAS 是怎么实现的呢?由于它是原子的硬件指令完成的,这里只能使用伪代码来辅助了解整个过程。
boolean CAS(A ,B , C){
if(A == B){
A == C;
return true;
}
return false;
}
在实际操作中,上面执行的过程是原子的,就是一次性梭哈,不存在线程安全问题。但是问题来了,这有啥用呢?这咋一看也就这样,其实它有很多实际应用的。
三、CAS 的应用
(一)实现原子类
在Java中的标准库中有一个包: java.util.concurrent.atomic
。这个包中提供了各种的原子类,这里的原子类大都是用 CAS 来实现的,有的时候使用这些类就不用加锁了。其中典型的就是 AtomicInteger
比如:
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
//预期:对 count 实现自加操作 100000 次
AtomicInteger count = new AtomicInteger(0);
Thread thread1 =new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
//自加
count.getAndIncrement();
}
}
});
Thread thread2 =new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
//自加
count.getAndIncrement();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count.get());
}
}
上面是通过两个线程来实现自加操作。这要是直接使用 “int” 类型的 “++”操作,最后的结果就不会是 100000 了。具体的原因就是“++”操作不是原子的。(以前的文章介绍过:Java中的线程安全 与 synchronized、volatile关键字 - 掘金 (juejin.cn))
(ps:这里就不介绍AtomicInteger的使用了,去查查文档就好了。)
下面给出用 CAS 实现原子自增的伪代码:
class AtomicInteger {
private int value;
public AtomicInteger(int value){
this.value = value;
}
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
//如果 CAS 失败就将 oldValue 更新,达到 oldValue 与 value 一致。
oldValue = value;
}
return oldValue;
}
}
上面代码看起来很复杂,不过别怕,下面咱们一步步来看:(两个线程同时调用getAndIncrement()
方法)
-
CAS(value, oldValue, oldValue+1)
的意思是:当value == oldValue
时,value = oldValue + 1
。 -
两个线程都读取
value
的值到oldValue
中。
- 线程 1 先执行 CAS 操作,由于
oldValue
和value
的值相同,直接进行对value
赋值。
- 线程 2 再执行 CAS 操作,第一次 CAS 的时候发现
oldValue
和value
不相等,不能进行赋值。因此需要 进入循环,在循环里重新读取value
的值赋给oldValue
。
- 线程 2 接下来第二次执行 CAS,此时
oldValue
和value
相同,于是直接执行赋值操作。
- 一直执行下去,直到
value = 100000
。
你会发现上面流程的关键就是 CAS 操作,当 value == oldValue
时必能让 value++
,这正是因为它是原子的;而当value != oldValue
时,则通过循环来确保 value == oldValue
。
(二)实现自旋锁
下面时自旋锁的伪代码:
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(this.owner, null, Thread.currentThread())
表示:检测当前的 owner 是否为 null,如果是 null 就进行交换,就是把当前线程的引用赋值给 owner,这时加锁成功;
反之,如果当 owner != null
的时候,说明该锁已经被其它线程获取了,这时开始循环(自旋),直到 owner
为空(被其它线程释放)。
四、CAS 的 ABA 问题
要知道世界上没有十全十美的东西,任何事都有其缺陷。 CAS 同样如此,我们再来想一想 CAS 的执行流程:检测 value 和 oldValue 是否一致。如果一致,就视为 value 中途没有被修改过,就进行下一步交换操作。但是!这里的一致是否就真的代表没有被修改过?
假设存在两个线程 t1 和 t2,有一个共享变量 value,初始值为 A。接下来,线程 t1 想使用 CAS 把 value 值改成 Z, 那么就需要:
- 先读取 value 的值, 记录到 oldNum 变量中。
- 使用 CAS 判定当前 value 的值是否为 oldNum = A, 如果相等, 就修改成 Z。
但是,在 t1 执行这两个操作之间,t2 线程可能把 value 的值从 A 改成了 B,又从 B 改成了 A 。这时 value 其实已经被修改过了,只不过又变回来了,这就是 ABA 问题。
这时可能有人会说:既然都已经变回来了,值反正都没变,t1 还是可以修改的呀。
真的可以修改吗?
(一)ABA 问题带来的 BUG
假设 小明
银行卡有 100 元
,我们假定银行转账操作就是一个 CAS 命令。现在 小明
给 小红
转 100元
(CAS(100,100,0)
),这时候因为网络原因 ATM1 卡住了
,小明就换了第二台 ATM2 继续转
。现在 小明 已经转了 100 元
给小红,他的银行卡剩 0 元
。这时 小明的妈妈 转给 小明 100元
,现在银行卡剩 100 元
。但好巧不巧,ATM1 网络恢复了
,执行 CAS 操作:CAS(100,100,0)
,发现银行卡剩 100 元,执行成功,小明实际上就给 小红 转了 200 元。
以上就是 ABA 问题可能带来的 BUG。我们如何规避上面的事情发生呢?
(二)解决方案:引入版本号
给要修改的值,引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。 CAS 操作在读取旧值的同时,也要读取版本号。
- CAS 操作在读取旧值的同时,也要读取版本号。
- 修改的时候:
- 如果当前版本号和读到的版本号相同,则修改数据,并把版本号加 1。
- 如果当前版本号高于读到的版本号,就操作失败(认为数据已经被修改过了)。
对于上面的案例,引入一个版本号 ver = 1,并将它与余额绑定。
- ATM1 读取 余额 100,ver = 1;ATM2 读取 余额100,ver= 1;
- 小明第一次成功转给小红 100,ver = 2;
- 小明妈妈转给小明 100,ver = 3;
- ATM2 网络恢复尝试转账,虽然余额一致,但是发现 ver 不一致,拒绝转账。
(可以把 ATM1、ATM2 看成线程)
这就是解决 ABA 问题的方法。