【JavaEE】CAS原理

目录

前言

什么是CAS?

 如何使用CAS?

CAS实现自旋锁

CAS的ABA问题

面试题

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

2.ABA问题怎么解决?


前言

在多线程中,多个线程同时对一个共享变量进行读写操作,那么就会出现线程安全问题,那就得使用synchronized进行加锁,但我们加锁,可能会出现死锁等问题。所以今天我们就来认识一种不需要加锁,也能实现原子性的算法--CAS

什么是CAS?

CAS (Compare and Swap) 是一种无锁算法中的原子操作,在多线程环境下用于实现数据的原子更新。CAS包含了三个操作数:内存值V旧的预期值A、和需要修改的新值B。当进行CAS操作时,会将内存值V和旧的预期值A进行比较,如果V和A相等,那就把内存值V更新为新值B;如果不相等,那么就不进行任何操作。

在多线程并发编程中,如果线程之间的锁冲突较低的情况下,那么就可以使用CAS,性能会优于synchronized,避免了线程上下文切换的开销

CAS是由CPU上的一条指令完成的,具有原子性。

我们来看下CAS伪代码,方便理解:

/**
     * 比较并交换地址处的值
     * 此方法用于原子地比较地址处的值是否与期望值相等,如果相等,则将地址处的值替换为新值
     * 这是一个基本的同步原语操作,常用于实现线程安全的算法和数据结构
     * 
     * @param address 要访问的内存地址
     * @param expectValue 期望值,即认为当前地址处应该具有的值
     * @param swapValue 如果地址处的值等于期望值,则用此值替换地址处的值
     * @return 如果地址处的值等于期望值,则返回true,否则返回false
     */
    boolean CAS(address, expectValue, swapValue) {
        // 检查地址处的当前值是否与期望值相等
        if (&address == expectedValue){
             // 如果相等,则将地址处的值替换为新值,并返回操作成功
            &address = swapValue;
            return true;
        }
        // 如果地址处的值与期望值不相等,则返回操作失败
        return false;
    }

CAS其实是CPU中的指令,但被操作系统、JVM 等层层封装后提供给上层使用。在java中,CAS操作主要是通过java.util.concurrent.atomic包中的原子类来实现的,如 AtomicIntegerAtomicLong等。 

我们来看AtomicInteger的中getAndIncrement(相当于前置++)方法的伪代码:

    /**
     * 原子整型类,提供原子操作的方法
     * 用于在多线程环境下安全地操作整型数值,防止数据不一致的情况
     */
    class AtomicInteger {
        // 原子整型变量的当前值
        private int value;

        /**
         * 获取当前值并原子性地进行自增
         * 
         * 此方法保证在多线程环境下安全地读取和更新整型变量的值
         * 使用CAS操作来实现原子性,直到CAS成功为止
         * 
         * @return 当前值(自增前的值)
         */
        public int getAndIncrement() {
            // 读取当前值,准备进行CAS操作
            int oldValue = value;
            // 使用CAS操作尝试将value值从oldValue递增为oldValue+1,直到成功为止
            while (CAS(value, oldValue, oldValue + 1) != true) {
                // CAS操作失败时,重新读取value值,确保数据一致性
                oldValue = value;
            }
            // 返回自增前的值
            return oldValue;
        }
    }

假设现在有线程t1和t2,要对某个变量进行++操作(分为读写存三个指令),那么没有使用CAS之前,当t1刚读取完值,此时切换到线程t2,当t2执行完指令,此时值为1,当由于此时t1线程原先读取到的旧值为0,那么此时t1线程继续执行指令,就会覆盖到t2的值,此时就会出现线程安全问题。

这里我们加上CAS操作之后,在t1线程读取完值后,此时切换到线程t2,t2在读取完内存值和期望的旧值之后,判断相等,将value更新为1。线程2执行完操作,切换到t1线程,此时t1线程判断内存值=1和期望的旧值=0,发现不相等,就不会进行交换操作,重新进行一次load操作。此时vaue为1,且oldvalue也为1,进行更新操作,value=2。

 如何使用CAS?

在java中,给我们提供了java.util.concurrent.atomic供我们使用。我们可以在java8查看一下atomic包中相关的方法。

Overview (Java Platform SE 8 ) (oracle.com)

如果你想要对int类型的数据进行操作,那么就可以创建一个AtomicInteger类,若想对Boolean类型的数据进行操作,那就创建一个AtomicBoolean的原子类。 

这里我们拿AtomicInteger为例

示例:假设现在有两个线程t1和t2,我们想使用使用者两个线程对count进行++操作,最终值为10000。

如果我们不进行任何加锁操作,那么就会有线程安全问题。

class Demo{
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++){
                count++;
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
                
    }
}

那么我们就会得到一个小于10000的值

所以我们可以用AtomicInteger。由于是个原子类,所以我们需要创建一个原子类对象。

这里我们可以指定初始值或者默认值为0。

 我们想要进行++操作,这里由于count是个类对象,不能直接++,需要调用其中的方法,这里前置++对应的方法是incrementAndGet(),后置++则是getAndIncrement()

class Demo{
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count=new AtomicInteger(0);
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++){
                count.getAndIncrement();
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());

    }
}

相同的,如果我们想要将count=10000减为0,那么我们可以使用getAndDecrement,

class Demo{
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count=new AtomicInteger(10000);
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++){
                count.getAndDecrement();//count--
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                count.getAndDecrement();//count--
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());

    }
}

当然,在AtomicInteger中也有很多其他的方法,以下是常用的方法:

这里就不做一一介绍,想了解其中其他方法,可以点击Java 平台 SE 8 (oracle.com) 查看。

 我们知道,++和--是不具有原子性,那么这里getAndIncrement() 和getAndDecrement为什么具有原子性?

我们可以ctrl+鼠标左键进入getAndIncrement方法内部

我们可以看到在 getAndIncrement内部又调用了getAndAddInt方法,而这个方法是属于U,这U就是unsafe类,这个类下的方法都是偏底层且危险的操作。

 

我们接着点击weakCompareAndSetInt方法。

点击compareAndSetInt方法查看。 

 可以看到compareAndSwapInt 方法是 native 修饰的本地方法,这个方法是 JVM 底层由 C/C++ 写的,我们是看不到的。

伪代码演示getAndIncrement

   /**
     * 原子整型类,提供原子操作的方法
     * 用于在多线程环境下安全地操作整型数值,防止数据不一致的情况
     */
    class AtomicInteger {
        // 原子整型变量的当前值
        private int value;

        /**
         * 获取当前值并原子性地进行自增
         * 
         * 此方法保证在多线程环境下安全地读取和更新整型变量的值
         * 使用CAS操作来实现原子性,直到CAS成功为止
         * 
         * @return 当前值(自增前的值)
         */
        public int getAndIncrement() {
            // 读取当前值,准备进行CAS操作
            int oldValue = value;
            // 使用CAS操作尝试将value值从oldValue递增为oldValue+1,直到成功为止
            while (CAS(value, oldValue, oldValue + 1) != true) {
                // CAS操作失败时,重新读取value值,确保数据一致性
                oldValue = value;
            }
            // 返回自增前的值
            return oldValue;
        }
    }

CAS实现自旋锁

在前面锁策略中,我们已经了解了什么是自旋锁,自旋锁就是当线程去获取锁时,此时锁被其他线程所持有,此时线程不会进入线程等待状态,而是会占用CPU资源,反复判断这个锁是否被释放,直到拿到锁为止。整个操作处于用户态,减少内核态的一些操作。

接下来,我们来使用CAS来实现自旋锁。

public class SpinLock{
    //用来记录锁被哪个线程持有,当owner为null时,说明没有线程持有锁,可被获取
    private Thread owner=null;

    //while循环来判断当前线程是否获取到锁,当owner为null时,说明没有线程持有锁,可被获取
    //反之,则说明此时锁被其他线程占用,无法获取到,返回false,取反之后就为true,进入死循环
    //直到获取到锁。当获取到锁后,owner被设置为当前线程,返回true,取反后就为false,退出循环
    public void Lock(){
        while(!cas(owner,null,Thread.currentThread())){

        }
    }

    /**
     * 解锁当前对象的锁状态
     * 通过将owner属性设置为null,表示当前对象不再被任何线程锁定
     */
    public void unLock(){
        this.owner=null;
    }
}

CAS实现的自旋锁适用于那些临界区代码执行时间非常短、并且锁的竞争不是很激烈的场景。

CAS的ABA问题

cas是通过判断内存中的数据和寄存器中的数据是否相等来更新值的,但有一种潜在的情况:这个内存中的数据由A->B->A,也就是内存数据被修改了一次,最后又修改为原来的数据。这种情况会出现问题吗?

我们来举个例子:假设我有5000块钱,我想要给我的好兄弟通过支付宝转500块钱,当我找到我好兄弟的账号后点击转账500元,但此时由于网络延迟问题,我又点击了一次,当网络好转之后,显示转账成功,但是在后台可能提示我转账两次,实际我只想转一次,但点击两次那么支付宝的后台就会有两个线程在执行cas操作。

对于上面这种情况,一般是不会出现问题的。

如果我们在转账成功之前,由其他人给我转进500块钱,会发生什么?

 可以看到,当我给我的好兄弟转了500之后,此时因为有其他人在此时又给我转了500,那在t1线程中,就判断我的钱还没有转,就又一次给我的好兄弟转了500,这样一共就给我的好兄弟转了1000块钱了。

那么如何防止这种情况发生呢? 

造成ABA问题就是因为变量能加也能减,如果只能加不能减或者只能减不能加,就不会有这种情况,因此,我们引入一个变量:版本号,如果我们对余额修改一次,那么版本号就+1,当cas在执行的时候,通过判断当前版本号和预期的旧的版本号是否相等,若相等,则说明还没有进行转账,若不相等,说明中间穿插了其他的修改操作,不进行修改操作。

/**
 * Test类用于演示并发下的资金转账操作
 * 通过CAS操作保证原子性,防止数据一致性问题
 */
class Test{
    // 账户余额,初始值为5000
    private int value=5000;

    // 版本号,用于CAS操作判断数据是否被修改
    private int version=0;

    /**
     * 转账方法,将指定金额添加到账户余额中
     * @param money 要转账的金额
     */
    public void transfer(int money){
        // 读取当前版本号
        int oldVersion=version;
        // 尝试更新版本号,确保操作的原子性
        if(CAS(version,oldVersion,oldVersion+1)) {
            // 更新成功后,增加账户余额
            value+=money;
        }
    }
}

面试题

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

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

2.ABA问题怎么解决?

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


以上就是本篇所有内容,若有不足,欢迎指正~

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猪同学hy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值