文章目录
- CAS
- 1. 什么是CAS
- 2. CAS的应用
- 2.1 实现原子类
- 2.2 实现自旋锁
- 3. CAS的ABA问题
- 3.1 什么是ABA问题
- 3.2 ABA问题引来的BUG
- 3.3 解决方案
CAS
1. 什么是CAS
CAS: 全称Compare and swap, 字面意思:”比较并交换“. 操作:
设V为内存中的值, A为寄存器中的值(旧的预期值), B也为寄存器中的值(需要修改的新值)
- 比较V和A是否相等(比较)
- 如果相等, 则将B赋给V, 返回true(交换)
- 如果不相等, 返回false
CAS伪代码
boolean CAS(address, expectValue, swapValue) {
if (*address == expectedValue) {
*address = swapValue;
return true;
}
return false;
}
CAS是一个原子的CPU指令完成的, 这个指令不可拆分, 上述伪代码不是原子的.
它既然是原子的, 那么我们编写线程安全的代码时, 就可以借助CAS
所以, 基于CAS又能衍生出一套"无锁编程"
但是CAS的适用范围具有一定的局限性, 只能针对一些特定场景进行优化.
当多个线程同时对某个资源进行CAS操作, 只能有一个线程操作成功, 但是并不会阻塞其他线程, 其他线程只会收到操作失败的信号.
2. CAS的应用
2.1 实现原子类
我们在前文讲解线程安全问题时, 曾举过一个例子, 就是多个线程针对一个count变量++, 如果不进行加锁, 那么就会引发线程安全问题. 当时我们是使用的synchronized
解决的问题.
其实我们还可以使用原子类, 可以更高效更简单的操作.
标准库中提供了java.util.concurrent.atomic
包, 里面的类都是基于CAS来实现的原子类.
AtomicInteger, AtomicLong
提供了自增/自减/自增任意值/自减任意值. 这些操作就是基于CAS按照无锁编程的方式实现.
//例子
public class Demo26 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
//count++
count.getAndIncrement();
// //++count
// count.incrementAndGet();
// //count--
// count.getAndDecrement();
// //--count
// count.decrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.get());
}
}
伪代码实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
//这里的比较, 为的是检查当前value是否被其他线程改变了.
//如果相等, 证明没有被改变, 操作还是原子的
//如果不相等, 证明被其他线程改变了, 此时要立即重新读取内存的值, 然后再次比较相等...
while ( CAS(value, oldValue, oldValue+1) != true) {
//如果value!=oldvalue, 那么就更新oldvalue, 继续CAS
oldValue = value;
}
return oldValue;
}
}
当两个线程并发的进行++操作时:
- 如果不加任何限制, 会引发线程安全问题
- 加锁保证线程安全: 通过锁, 强制两个++操作串行执行
- 原子类/CAS保证线程安全: 借助CAS来识别当前是否出现了并发/穿插的情况.
- 如果没穿插, 直接修改, 是安全的
- 如果穿插了, 就重新读取内存中最新的值, 再次尝试修改.
2.2 实现自旋锁
自旋锁伪代码
public class SpinLock {
//使用owner表示当前是哪个线程持有这把锁
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问题
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
账户里面有100元, 要取款50
假设出现了极端情况
摁第一次取款的时候, 卡了一下, 然后又摁了一次
这样便产生了两个"取款请求", ATM使用两个线程来处理这两个请求
假设按照CAS的方式进行取款, 每个线程这样操作:
- 读取账户余额, 放到变量M中
- 使用CAS判定当前实际余额是否是M, 如果是就把实际余额改成M-50, 如果不是, 放弃操作
正常的过程
-
存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
-
线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
-
轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.
异常的过程
-
存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
-
线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
-
在线程2 执行之前, 朋友正好往转账 50, 账户余额变成 100
-
轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作
这个时候, 扣款操作被执行了两次
t1扣款50, t3又增加50, 就让t2错误的认为没有线程修改过余额, 就又扣款了50.
这就是A-B-A问题产生的后果
3.3 解决方案
我们可以给要修改的值, 引入版本号, 让它只增长不减小.在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
-
CAS 操作在读取旧值的同时, 也要读取版本号.
-
真正修改的时候,
-
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
-
如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
-