CAS机制实现原理分析

Posted Ariel_欢

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CAS机制实现原理分析相关的知识,希望对你有一定的参考价值。

学习方法:场景->需求->解决方案->应用->了解原理

一、CAS是什么?

CAS机制:CompareAndSwap 或 CompareAndExchange 或 CompareAndSet。
CAS是一个能够进行比较和替换的方法,这个方法能够在多线程环境下保证对一个共享变量进行修改时的原子性不变。

场景:i++ 保证原子性

为了更好的理解CAS机制,我们先看一个例子:

public class S01_AtomicDemo 
    volatile int i=0;
    //加上 synchronized 关键字保证结果一定是 2000 正确的
    public /*synchronized */ void incr()
        i++;
    

    public static void main(String[] args) throws InterruptedException 
        S01_AtomicDemo s01_atomicDemo = new S01_AtomicDemo();
        Thread[] threads = new Thread[2];

        for (int j=0; j <2 ; j++) 
            threads[j]=new Thread(()->
                for (int k = 0; k < 1000; k++) 
                    s01_atomicDemo.incr();

                
            );

            threads[j].start();
        

        threads[0].join();//保证线程执行结束
        threads[1].join();
        System.out.println(s01_atomicDemo.i);

        //期望结果是 两个线程分别执行 1000  i++,结果应该是 2000
        //但是实际结果小于2000,这是原子性问题——可能第一个线程已经加到i=100了,第二个线程读的还是i=0
    

这个例子的结果:

  • 期望结果是 两个线程分别执行 1000 i++,结果应该是 2000
  • 在不加synchronized锁的同步锁的情况下,实际结果小于2000,这是原子性问题——可能第一个线程已经加到i=100了,第二个线程读的还是i=0

想到达结果正确,可以从两个方面考虑:

  • ① 不允许当前非原子指令在执行过程中被中断,也就是说保证i++操作在执行过程中不存在上下文切换。
  • ② 多线程并发执行导致原子性问题可以通过一个互斥条件来实现串行执行

synchronized锁可以实现。增加synchronized锁之后可以保证原子性(结果正确),但是加锁会存在性能问题。

需求:那除了加锁,还有没有更好的方式呢?

这个时候我们想到一种乐观锁的机制:在线程调用i++之前,先判断i的值和之前读取的 i 的预期值是否相等。如果相等,则说明 i 的值 没有被其他线程修改过,这个时候可以正常修改;否则,表示修改过,就要重新读取最新的 i 的值进行累加。

解决方案:CAS就是解决这个问题的。

应用:getAndIncrement()方法

public class S01_AtomicInteger 
    public static void main(String[] args) throws InterruptedException 
        AtomicInteger atomicInteger=new AtomicInteger(0);
        Thread[] threads=new Thread[2];
        for (int j = 0; j < 2 ; j++) 
            threads[j]=new Thread(()->
                for (int i = 0; i < 1000; i++) 
                    /**
                     * getAndIncrement是原子累加方法,每次调用一次会在原来值的基础上+1,这个过程采用了CAS机制保证原子性。点进去看源码
                     */
                    atomicInteger.getAndIncrement();
                
            );
            threads[j].start();
        
        threads[0].join(); //保证线程执行结束
        threads[1].join();
        System.out.println(atomicInteger); //结果是2000,没问题!
    

二、CAS原理示意图

该图表示通过CAS对变量V进行原子更新操作。CAS方法中会传递三个参数,第一个参数V表示要更新的变量,第二个参数E表示期望值,第三个参数U表示更新后的值。更新的方式是,如果V==E,表示预期值和实际值相等,则将V修改成U并返回true,否则修改失败返回false。
在Java中的Unsafe类中提供了CAS方法,针对int类型变量的CAS方法定义如下。


从方法定义中可以看到,它有四个参数:

  • o,表示当前的实例对象。
  • offset,表示实例变量的内存地址偏移量(内存中实际值)。
  • expect,表示预期值(更新前的值)。
  • update,表示要更新的值(更新后的值)。

expect和update比较好理解,offset表示目标变量X在实例对象o中内存地址的偏移量。简单来说,在预期值expect要和目标变量X进行比较是否相等的判断中,目标变量X的值就是通过该偏移量从内存中获得的。 

 三、CAS在AtomicInteger中的应用以及源码分析


z通过CAS在AtomicInteger中的应用来理解CAS机制和源码:

public class S01_AtomicInteger 
    public AtomicInteger atomicInteger=new AtomicInteger(0);
    public void add()
        /**
         * getAndIncrement是原子累加方法,每次调用一次会在原来值的基础上+1,这个过程采用了CAS机制保证原子性。点进去看源码
         */
        atomicInteger.getAndIncrement();
    

其中,valueOffset表示AtomicInteger中的成员变量value在内存中的偏移量,后续会用它直接从内存中读取value属性当前的值。
valueOffset的初始化方法如下( 点击valueOffset): 

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static 
    try 
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
     catch (Exception ex)  throw new Error(ex); 


private volatile int value;

valueOffset用到了unsafe.objectFieldOffset()方法,获取value字段在AtomicInteger.class中的偏移量。

个人理解: objectFieldOffset(AtomicInteger.class.getDeclaredField("value"))方法,使用反射(getDeclaredField)来获取value字段在AtomicInteger.class中的属性值。

结合这段代码的分析,对前面提到的o和offset这两个字段的含义就不难理解了,o 表示当前的实例对象,offset 表示要更新的变量(个人理解:在AtomicInteger中是指value字段)。
在CAS中,我们需要通过expect去和某个字段的值进行比较,而expect比较的目标值就是通过offset找到某个字段在内存中的实际值(在AtomicInteger中是指value字段),如果相等,就修改成update并返回true,否则返回false。
unsafe.getAndAddInt(this,valueOffset,1)的源码定义:  

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;

代码整理(形参名):

 

public final int getAndAddInt(Object o,long offset,int n)
    int v;
    do 
        v=this.getIntVolatile(o,offset);
    while (!(this.compareAndSwapInt(o,offset,v,v+n)));
    return v;

 代码实现逻辑分析如下:
●“v = this.getIntVolatile(o, offset); ”表示根据value在对象o的偏移量来获得当前的值v。
●使用compareAndSwapInt()方法实现比较和替换,如果value当前的值和v相等,说明数据没有被其他线程修改过,则把value修改成v+n。
●这里采用了循环来实现,原因想必大家能猜测到。如果compareAndSwapInt()方法执行失败,则说明存在线程竞争,但是当前的方法是进行原子累加,所以必须要保证成功,为了达到这个目的,就只能不断地循环重试,直到修改成功后返回。
●整体来说,CAS就是一种基于乐观锁机制来保证多线程环境下共享变量修改的原子性的解决方案。前面分析的案例虽然是在Java中的应用场景,但是它本质上和synchronized同步锁中用到的CAS是相同的,我们来看一下Unsafe类中CAS的定义。

public final native boolean compareAndSwapInt(Object o, long offset, int expect, int update);

compareAndSwapInt()是一个native方法,该方法是在JVM中定义和实现的。

四、总结:i++对应CAS示意图进行详解:

在Java中的Unsafe类中提供了CAS方法,针对int类型变量的CAS方法定义如下。



从方法定义中可以看到,它有四个参数: ● o,表示当前的实例对象。 ● offset,表示实例变量的内存地址偏移量(内存中实际值)。 ● expect,表示预期值(更新前的值)。 ●update,表示要更新的值(更新后的值)。 expect和update比较好理解,offset表示目标变量X在实例对象o中内存地址的偏移量。简单来说,在预期值expect要和目标变量X进行比较是否相等的判断中,目标变量X的值就是通过该偏移量从内存中获得的。

 

offset是要修改的value字段(内存中实际存的值),expect期望值是value字段从内存中取出来时存在的值(对i++操作来说,即是i进行++之前的值),如果相等,则修改为v+n。

以上是关于CAS机制实现原理分析的主要内容,如果未能解决你的问题,请参考以下文章

CAS机制实现原理分析

Java CAS 原理分析

乐观锁实现之CAS算法分析

深入浅出Java并发编程指南「原理分析篇」非常全面的的探索和分析AQS的基本原理和实现机制(道法器术篇)

CAS操作原理分析

JAVA CAS原理深度分析(*)