Java Web 实战 12 - 多线程进阶之 CAS 问题

大家好 , 这篇文章给大家带来的是多线程当中的 CAS 问题
CAS 是 操作系统 / 硬件 给 JVM 提供的另外一种更轻量的原子操作的机制
推荐大家跳转到 此链接 查看效果更佳~
上一篇文章的链接我也给大家贴到这里了
点击即可跳转到文章专栏~
在这里插入图片描述

CAS 问题

CAS 是 操作系统 / 硬件 给 JVM 提供的另外一种更轻量的原子操作的机制
CAS 是 CPU 提供的一个特殊指令 : compare and swap (比较和交换是一条指令 -> 原子的)
compare : 比较内存和寄存器的值 . 如果相等 , 则把寄存器和另一个值进行交换 ; 如果不想等 , 不进行操作

1. CAS 伪代码

// address:内存地址
// expectValue:用来比较的值/预期值(存放在寄存器中)
// swapValue:用来交换的值(存放在另一个寄存器中)
boolean CAS(address, expectValue, swapValue) {
    
    
    // 拿内存地址对应的值和预期值比较
    // 一样进行交换
    if (&address == expectedValue) {
    
    
        // 另一个寄存器的值和内存中的值进行交换
        &address = swapValue;
        return true;
    }
    return false;
}

上面的代码 , 我们看着并不是原子的
但是这一系列操作都是由一个 CPU 指令来完成的

2. 典型应用场景

2.1 使用 CAS 实现原子类

原子类是标准库中提供的一组类 , 这个类可以让原子进行 ++ – 等运算

我们之前完成过这个代码

public class Demo27 {
    
    
    public static int count = 0;

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                count++;
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count=" + count);
    }
}

这个代码得到的值是错误的
这是因为线程不安全导致的问题
我们可以使用 synchronized关键字解决这个问题
但是我们还有更好的方式

import java.util.concurrent.atomic.AtomicInteger;

public class Demo27 {
    
    
    // public static int count = 0;
    // AtomicInteger:原子类整数
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                // 相当于count++;
                count.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                // 相当于++count
                count.incrementAndGet();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count=" + count);
    }
}

image.png
这个原子类是怎样实现的呢 ?
来看这段伪代码

class AtomicInteger {
    
    
    private int value;
    
    public int getAndIncrement() {
    
    
        // 先把旧的 value 值存储下来
        // oldValue 就相当于寄存器(因为在代码中没有直接的寄存器)
        int oldValue = value;
        // 判断新读取到的 value 值(内存中的值)和旧的 value 值(寄存器中的值)是不是一样的
        // 一样的就把寄存器(oldValue)的值+1,再和内存中的值进行交换
        // 不一样的话就进入循环,重新读取 value 的值
        while ( CAS(value, oldValue, oldValue + 1) != true) {
    
    
            oldValue = value;
        }
        return oldValue;
    }
}

image.png

2.2 使用 CAS 实现自旋锁

先来看伪代码

public class SpinLock {
    
    
    // 表示当前是由谁来加锁
    private Thread owner = null;

    // 通过 CAS 看当前锁是否被某个线程持有.
    // 如果这个锁已经被别的线程持有, 那么就自旋等待.
    // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
    public void lock(){
    
    
        // 判断当前 owner 是否为空
        // 为空:代表当前没加锁,那就要进行交换,把当前这里要去加锁的值赋值到 owner 里面了
        // 不为空:说明当前的锁被其他线程占用了, CAS 就返回 false,循环继续进行,就变成了自旋的状态;
        // 直到 owner 的值被设置成 null 了,我们 CAS 才能完成交换,退出循环
        while(!CAS(this.owner, null, Thread.currentThread())){
    
    
        }
    }
    
    public void unlock (){
    
    
        this.owner = null;
    }
}

3. CAS 的 ABA 问题

举个栗子 :
我们作为一个普通的老百姓 , 在网上买个手机 , 我们正常是无法区分这是一台新机还是一台翻新机
类似的 , 在 CAS 中 , 也无法区分 , 数据始终就是 A , 还是从 A -> B -> A , 后面的这种情况 , 就很有可能出现问题

滑稽老哥有 1000 块钱存款 , 他想从 ATM 中取走 500
我们假设 ATM 按照 CAS 的方式来进行操作
滑稽老哥取钱的时候 , 按下取款按钮 , 就会触发一个 “取钱线程” , 但是滑稽手一滑 , 连续按了两下 , 就产生了两个线程
image.png
线程 3 这种情况 , 是极端情况下的小概率事件
没有合适时机切入的线程 3 , 也就不存在正好把值改成原来的值
那么 ABA 一般也不会有 bug

针对 ABA 的解决 , 有很多种办法
正经的解决 ABA 问题的办法 , 是想办法获取到中间的过程
通过引入 “版本号” 来解决这个问题
上面的 CAS , 是比较余额 , 余额相等就可以修改 , 但是余额是可以变大也可以变小 , 因此就有可能出现 ABA 问题
如果换成版本号 , 约定版本号只能增不能减 , 就可以避免 ABA 问题
image.png

4. 相关面试题

  1. 讲解下你自己理解的 CAS 机制

全称 Compare and swap , 即 “比较并交换”. 相当于通过一个原子的操作 , 同时完成 “读取内存 , 比
较是否相等 , 修改内存” 这三个步骤 . 本质上需要 CPU 指令的支撑 .

  1. ABA 问题怎么解决 ?

给要修改的数据引入版本号 . 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期 .
如果发现当前版本号和之前读到的版本号一致 , 就真正执行修改操作 , 并让版本号自增 ; 如果发现当前版本号比之前读到的版本号大 , 就认为操作失败 .

CAS 问题就给大家讲解完毕
有帮助的话请一键三连~
Java Web 实战 11 - 多线程进阶之常见的锁策略

猜你喜欢

转载自blog.csdn.net/m0_53117341/article/details/129567447