CAS-比较并交换

CAS的概念

CAS的全称是Compare-And-Swap,它是一条cpu并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。

CAS并发原语体现在Java语言中就是sun.miscUnsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令,这是一种完全依赖于硬件功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。

CAS的作用:CAS可以将比较和交换转为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中值v,旧的预估值x,要修改的新值B,如果旧的预估值x等于内存中的值v,就将新的值B保存到内存中,否则什么都不做或重来,当它重来重试的这种行为为自旋。

CAS之自旋锁(Spinlock)

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
循环时,当操作成功返回true,循环结束;当返回false时,接着执行循环,继续尝试CAS操作,直到返回true.

CAS之Unsafe类:

CAS的底层实现是通过unsafe类来实现的。Unsafe类介绍:
在这里插入图片描述
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
图中的value值用volatile修饰,保证了多线程之间的可见性
图中的变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
在这里插入图片描述
在这里插入图片描述

原子类中使用CAS

1、首先使用AtomicInteger创建一个实例,并初始化为5

//创建一个原子类
AtomicInteger atomicInteger=new AtomicInteger(5)

2、然后调用CAS方法,企图更新成2019,这里有两个参数,一个是5,表示期望值,第二个是我们要更新的值

atomicInteger.compareAndSet(5,2019);

3、然后再次使用此方法,将同样值改为1024

atomicInteger.compareAndSet(5,1024)

4、完成代码如下

public class CASDemo{
	public static void main(String[] args){
    	//创建一个原子类
        AtomicInteger atomicInteger=new AtomicInteger(5);
        
        /**
         * 一个是期望值,一个是更新值,但期望值和原来的值相同时,才能够更改
         * 假设三秒前,我拿的是5,也就是expect为5,然后我需要更新成 2019
         */
        System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());
    }
}

执行结果:
true  current data:2019
false current data:2019

这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经改成了2019,不满足期望值,因此返回了false,本次写入失败
在这里插入图片描述

CAS底层原理

首先我们先看看atomicInteger.getAndIncrement()方法的源码
在这里插入图片描述
valueOffset的内存地址是具体变量的value的内存偏移量,它是对象创建的时候,通过反射机制

//获取unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value的内存偏移量
 private static final long valueOffset;

//实际变量值,用volatile修饰,保证可见性
private volatile int value;


static {
        try {
            //获取value在AtomicInteger中的偏移量
            //objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }


在这里插入图片描述
在这里插入图片描述
getIntVolatile:通过对象的地址和value的内存偏移量获取到实际的数据
var5:就是我们从主内存中拷贝到工作内存中的值
那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较
假设执行 compareAndSwapInt返回false,那么就一直执行 while方法,直到期望的值和真实值一样
●val1:AtomicInteger对象本身
● var2:该对象值得引用地址
● var4:需要变动的数量
● var5:用var1和var2找到的内存中的真实值
○ 用该对象当前的值与var5比较
○ 如果相同,更新var5 + var4 并返回true
○ 如果不同,继续取值然后再比较,直到更新完成

这里没有用synchronized,而用CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的do while循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。

假设线程A和线程B同时执行getAndInt操作(分别跑在不同的CPU上)
1、AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份价值为3的副本,分别储存在各自的工作内存
2、线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起(该线程失去CPU执行权)
3、线程B也通过getIntVolatile(var1,var2)方法获取到value值也是3,此时刚好线程B没有被挂起,并执行了compareAndSwapInt方法,比较内存的值也是3,成功修改内存值为4,线程B打完收工,一切OK
4、这时线程A恢复,执行CAS方法,比较发现自己手里的数字3和主内存中的数字4不一致,说明该值已经被其他线程抢先一步修改过了,那么A线程本次修改失败,只能重新读取后在来一遍零 ,也就是执行do while
5、线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总能看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功

CAS的缺点:

1)ABA问题:因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A——>B——》A就会变成1A-----》2B----3A。
从Java1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
AtomicStampedReference内部不仅维护了对象值,还维护了一个时间戳。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳,对象值和时间戳都必须满足期望值,写入才会成功。

2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
从JAVA1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值