1. 什么是CAS
CAS全称:compare and swap ,即为 比较和交换。一个CAS涉及以下操作:
我们假设内存中的原数据 V, 旧的的预期值为 A ,需要修改为B
- 比较A与V是否相等(比较)
- 如果相等,将B写入V(交换)
- 返回是否交换成功
CAS伪代码:
boolean CAS(V, A, B) {
if (V == A) {
V = B;
return true;
}
return false;
}
这三个操作是通过单个cpu指令完成的.
2. CAS的应用
2.1 实现原子类
标准库中提供了 java.util.concurrent.atomic 包,⾥⾯的类都是基于这种⽅式来实现的.
典型的就是AtomicInteger类.其中的getAndIncrement相当于i++操作.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement()
伪代码实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
假设有两个线程同时修改value,由于每次调用CAS时我们都会把当前的value和之前的value传入CAS,如果value == oldValue 则才会对value进行更改,于是保证了该操作的原子性。
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问题
3.1 什么是ABA问题
假设存在两个线程t1和t2.有⼀个共享变量num,初始值为A.接下来.线程t1想使⽤CAS把num值改成Z,那么就需要先读取num的值,记录到oldNum变量中.使⽤CAS判定当前num的值是否为A,如果为A,就修改成Z.
但是,在t1执⾏这两个操作之间,t2线程可能把num的值从A改成了B,⼜从B改成了A线程t1的CAS是望num不变就修改.但是num的值已经被t2给改了.只不过⼜改成A了.这个时候t1究竟是否要更新num的值为Z呢?
到这⼀步,t1线程⽆法区分当前这个变量始终是A,还是经历了⼀个变化过程。
3.2 ABA 问题引起的BUG
⼤部分的情况下,t2线程这样的⼀个反复横跳改动,对于t1是否修改num是没有影响的.但是不排除⼀些特殊情况.
例如在ATM机的存取款操作:
假设 A 有1000存款,A想从ATM机里取出 500,此时机器卡了,A又点了以此取钱操作,于是就产生了两个线程尝试取钱,此时B在对A的卡里打500,这个操作刚好在 两次取钱操作的中间执行了,此时就导致,第二次取钱操作读取到的值还是1000,于是再次执行了取钱操作。
3.3 解决方案
给要修改的值引入版本号,在CAS比较当前数据和旧值时,同时对比版本号是否相同,如果两者都相同才修改数据,同时把数据版本号加1,版本号不同则认为数据被修改过。