【多线程】CAS 原理

【多线程】CAS 原理_第1张图片

1. 什么是CAS?

CAS 的全称是 Compare and swap 直译过来就是 比较并交换。

其实 CAS 是一个原子的硬件指令去完成比较并交换这个操作的,也就是 CAS 是 CPU 提供的一个特殊指令!

既然是原子的,也就是说 CAS 是可以保证线程安全的!

这里就来介绍下 CAS 的简单逻辑:

假设内存中的原数据为 V,旧的预期值是 A,需要修改的新值是 B (A和B是寄存器中的值)

  • 比较 A 与 V 是否相等 (compare)

  • 如果 A 与 V 相等,就将 B 写入 V (swap),不相等则无事发生

  • 返回当前的操作是否成功

其实 CAS 就是这么简单,看到这,你还不理解有点蒙蒙的很正常!接着往下看。


2. CAS 应用场景

2.1 实现原子类

回顾我们之前写过的一段代码:


public class Test {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}
// 第一次执行结果: 64628
// 第二次执行结果: 62853
// 第三次执行结果: 53330

大概就是两个线程并发的修改同一个变量的内容,引发的线程安全问题。

之前的方案是采用加锁来解决的,但是现在也可以使用 Java 提供的原子类保证上述代码的原子性了。

原子类,底层自增采用了 CAS 来保证原子性,下面我们就来简单的认识下 Java 提供的原子类:

在标准库中提供了 java.util.concurrent.atomic包,这里面实现的类都是基于 CAS 这种方式实现的,其中典型的就是 AtomicInteger 类,此时我们就能通过这个 AtomicInteger 类来改造上面的代码:


public class Test {
    private static AtomicInteger count = new AtomicInteger(0); // 初始值给 0
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                count.getAndIncrement(); // 相当于 count++
//                count.incrementAndGet(); // 相当于 ++count
//                count.getAndDecrement(); // 相当于 count--
//                count.decrementAndGet(); // 相当于 --count
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                count.getAndIncrement(); // 相当于 count++
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}
// 程序执行结果: 100000

本质上 getAndIncrement() 方法就是将 count++ 的操作转成了原子的,那么这个方法底层是如何使用 CAS 的呢?

这里我们就写一段伪代码来大致的了解下 getAndIncrement 方法的实现:


// 下述为伪代码, 并非真实代码
private int value;
public int getAndIncrement() {
    int oldValue = value; // oldValue 理解成寄存器 A
    while (CAS(value, oldValue, oldValue + 1) != true) {
        oldValue = value;
    }
    return oldValue;
}

这里我们就要了解 CAS(, , ,) 这里面三个参数的含义了,第一个参数 value 表示 V,第二个参数 oldValue 表示A,第三个参数表示 B。

value 是内存中的值,我们再来回顾下 CAS 的工作,如果 V 和 A 相等,就将 B 的 值与 V 的值交换,本质就是把 B 的值写入内存了,并返回 true,如果 V A 不相等返回 false!

正常情况下,value 和 oldValue 的值应该都是一样的吧!

但是我们要考虑多线程的情况,假设 t1 线程执行完 oldValue = value; 这一行的时候,CPU 去执行 t2 线程了,t2 修改了 value 的值,此时 CPU 又回来执行 t1 线程了,进入 while 循环后执行 CAS 操作(注意!CAS 操作是原子的),要重新读取 value 值,此时就能发现 value 值被改了,于是就能得出 V 和 A 不相等,于是返回 false,循环也就不会结束,此时就会重新将 value 的值赋值给 oldValue。由此一来,就能保证线程安全了!

原子类这里的实现,就是每次修改 value(内存) 之前,都会确认一下要修改的值是否改变了!

2.2 实现自旋锁

这里我们简单写一个实现自旋锁的伪代码:


private Thread owner = null;
public void lock() {
    while (!CAS(this.owner, null, Thread.currentThread())) {
        
    }
}
public void unlock() {
    this.owner = null;
}

相信这段代码也不难理解,lock() 方法就是加锁操作,会一直判断 owner 是否为 null,如果 V 和 A 相等,也就是该对象没有被加锁,此时就获取当前调用该方法的线程,令 owner = 当前线程对象,此时就相当于该线程获取到锁了。

如果 V 和 A 不相等,表示 owner 里面存了其他线程对象(其他线程已经获取到锁了),此时 owner 也和 null 不相等了,此时也就不会交换 B 与 V 的值,直到 unlock() 后,才有可能进行交换了。

上述就是自旋锁逻辑上的实现。

注意!上述原子类和自旋锁的伪代码中的 CAS 并不是 Java 给我们提供的方法,Java 原生的 CAS 方法比较复杂,此处相当于是一个简化的方式。


3. CAS 典型 ABA 问题

这个 ABA 问题也是面试中常见的一个问题,CAS 在运行中的核心就是检查 value 和 oldValue 的值是否一致,如果一致就视为 value 中途没有被改变,所以就可以进行下一步的交换操作。

但是我们仔细想想,会不会 value 被改变了,但又被还原回来了?此时细思极恐!

这就比如,张三之前谈过十几次恋爱,但对每一任都说之前没有谈过恋爱,把自己包装成恋爱小白,只要张三不露馅,所以张三的每一任都认为张三是初恋,这样问题可就大了呀!

ABA 问题大部分情况下都不会对代码/逻辑产生影响,但是不能排除一些极端情况!

此时我们就举一个不太现实的例子,但是能让大家更好的理解 ABA 问题!

例:滑稽小伙要去取钱,假设滑稽小伙卡里只有 10000 元

【多线程】CAS 原理_第2张图片

滑稽小伙准备一下子取 5000 出来(此时 ATM 使用 CAS 的方式来扣款),可是按下取款的一瞬间,机器卡了,滑稽小伙就手欠又按了一下,如果机器出 bug 了,就会触发两次扣款。

由于是 CAS 实现的,不会有问题,因为第二次扣款发现余额不对,也就不会触发扣款!

但是万一第一次执行扣款后,第二次扣款前,滑稽表哥给滑稽转了 5000 块钱呢?

【多线程】CAS 原理_第3张图片

此时就扣款了两次,但滑稽只得到了 5000 块钱,还亏了 5000,那么这种概率非常非常低!但也不保证完全不会出现!

针对上述的问题如何解决呢?

我们可以引入一个版本号,设想一下,初始的版本号是1,每次修改版本号都 +1,然后进行 CAS 的时候,就不是以金额为基准判断了,而是以版本号为基准,如果版本号没有发生变化,也就说明金额也没发生变化,如果版本号变了,那金额肯定被修改过。

注意:版本号只能增加,不能减少!

所以乐观锁在访问同一个变量的时候,如何判断当前是否有冲突呢?其实也是通过引入版本号的方式来判断是否访问冲突了!


下期预告:【多线程】synchronized 原理

你可能感兴趣的:(多线程从入门到精通(暂时限免),java,jvm,CAS)