什么是CAS/CAS的应用/CAS的ABA问题

文章目录

  • 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也为寄存器中的值(需要修改的新值)

  1. 比较V和A是否相等(比较)
  2. 如果相等, 则将B赋给V, 返回true(交换)
  3. 如果不相等, 返回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来实现的原子类.

什么是CAS/CAS的应用/CAS的ABA问题_第1张图片

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, 还是经历了一个变化过程.

什么是CAS/CAS的应用/CAS的ABA问题_第2张图片

3.2 ABA问题引来的BUG

账户里面有100元, 要取款50

假设出现了极端情况

摁第一次取款的时候, 卡了一下, 然后又摁了一次

这样便产生了两个"取款请求", ATM使用两个线程来处理这两个请求

假设按照CAS的方式进行取款, 每个线程这样操作:

  1. 读取账户余额, 放到变量M中
  2. 使用CAS判定当前实际余额是否是M, 如果是就把实际余额改成M-50, 如果不是, 放弃操作

正常的过程

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.

  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

  3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

异常的过程

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.

  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

  3. 在线程2 执行之前, 朋友正好往转账 50, 账户余额变成 100

  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作

这个时候, 扣款操作被执行了两次

t1扣款50, t3又增加50, 就让t2错误的认为没有线程修改过余额, 就又扣款了50.

这就是A-B-A问题产生的后果

3.3 解决方案

我们可以给要修改的值, 引入版本号, 让它只增长不减小.在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

  • CAS 操作在读取旧值的同时, 也要读取版本号.

  • 真正修改的时候,

    • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.

    • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

你可能感兴趣的:(Javaee,java,开发语言,CAS)