CAS解析与用法以及ABA问题

性质:

CAS全称CompareAndSwap,意为比较并交换,Compare(比较),Swap(交换)。

这里涉及到JMM(Java内存模型),变量等信息都是存储在主内存中,而对变量进行操作的过程是在工作内存中进行的,所以进行操作的时候会先将主内存中的拷贝到工作内存中。

这里的CAS指的就是在工作内存中,在操作修改之前保存一个快照(原值),修改完以后进行写回到主内存中,在此过程中会将刚刚在工作内存中保存的快照与主内存中的值进行比较(Compare),如果一致,则代表数据没有被修改,进行交换(Swap)返回true,反之则修改失败。

解析:

我们以AtomicInteger来做举例示范:

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

可以从上面的AtomicInteger的部分源码可以看出,compareAndSet方法中需要两个参数,第一个参数就是预期值,第二个参数就是更新值,意思就是如果主内存中的值与预期值相同才会修改成功。在调用unsafe类中的compareAndSwapInt方法中,this代表的对象,valueOffset表示对象在内存中地址(偏移量),通过this与valueOffset再次获取主内存中的值,然后与expect进行比较,一致则进行替换update。

unsafe类中的都是本地方法(native修饰),本地方法是直接调用系统底层资源进行操作,而unsafe中的CAS方法就是一条CPU并发原语,JVM帮我们实现出CAS汇编指令,它是一种完全依赖于硬件的功能,通过它实现了原子性,并且原语的执行必须是连续的不允许中断,也就是CAS是一条原子指令。

缺点:

我们使用CAS的场景经常是伴随着while循环进行使用的,所以在某些情况下循环消耗大,也就是如果一致失败就会一直循环,比如AtomicInteger的自增方法,调用的就是unsfe中的以下方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

其中var1代表的自增对象,var2代表内存地址,var4代表自增量(也就是1,每次加1),使用do-while循环,先通过var1与var2获取到内存中的值,并保存为var5(快照),在while中,再通过var1与var2获取内存中的值,并与刚刚获取到var5进行比较,如果一致,就将计算结果(var5+var4)写入,跳出循环,否则进行下一次循环,重新获取。所以如果一直失败就会一直循环进行消耗(这里是一个自旋锁)。

还有一个经典的问题就是ABA问题,通过上面的了解我们知道,从某种意义上说他比较的只是值,但是假如有两个线程,线程1取到值X以后(X=A),进行操作的过程中,线程2也也取到值X,并将X的值修改为B,写入到内存中,而后线程2又取到值X,并又修改回A,再写入内存,此时线程1才执行完操作,进行比较的时候发现 X的值A,它就会以为X没有修改过,所以修改成功,实际上X的值早就变动过了,这就是经典的ABA问题。再举一个例子,如果ABA出现在生活中的话,就很有可能出现一笔钱用过两次以后,系统觉得这笔钱是没人用过的,这是一个很大的漏洞!

解决方案:

针对ABA问题的解决方案就是增加一个类似于版本号的标记,比如AtomicStempedRefrence,它的内部维护了一个时间戳,就相当于版本号,给大家做个示例:

public class CASDemo{


    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100, 0);


    public static void main(String[] args) throws Exception{
        //x.compareAndSet(0, 1);

        new Thread(()->{
            int stemp=atomicStampedReference.getStamp();
            try
            {
                Thread.sleep(2000);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            boolean result=atomicStampedReference.compareAndSet(100, 200, stemp,stemp+1);
            System.out.println("线程:"+Thread.currentThread().getName()+"\t 修改结果:"+result+"\t 当前最新版本号:"+atomicStampedReference.getStamp());
        },"A").start();

        new Thread(()->{
            boolean result=atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("线程:"+Thread.currentThread().getName()+" 第一次修改\t 修改结果:"+result+"\t 当前版本号:"+atomicStampedReference.getStamp());
            result=atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("线程:"+Thread.currentThread().getName()+" 第一次修改回原值\t 修改结果:"+result+"\t 当前版本号:"+atomicStampedReference.getStamp());
        },"B").start();

    }



}

执行结果:

线程:B 第一次修改     修改结果:true     当前版本号:1
       线程:B 第一次修改回原值     修改结果:true     当前版本号:2
       线程:A     修改结果:false     当前最新版本号:2

从上面线程A中我们在最开始就获取了版本号,后面线程B修改两次后导致版本不符合要求,所以导致失败。具体大家可以看看AtomicStempedRefreence的用法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值