CAS原理

前言:

  今天在看Ribbon底层是如何实现负载轮询算法的时候看到了这一段,其中有这句代码nextServerCyclicCounter.compareAndSet(current, next),这个nextServerCyclicCounter就是AtomicInteger,那么这个AtomicInteger是个什么东西呢?

 

 

 

 

AtomicInteger原子类:

  我们知道java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。synchronized关键字可以保证可见性和有序性却无法保证原子性。而这个AtomicInteger的作用就是为了保证原子性。在多线程使用i++的时候很容易产生问题,当一个线程要操作一个数据的时候其实分为三步,第一从主内存中拿到数据,第二自己对数据进行修改,第三把修改后的数据写到主内存中,这种方式就可能会出现,两个或者多个线程同时拿到数据,修改完成之后直接写到内存就会产生与我们预期不符的结果。比如十个线程分别执行十次i++,最后结果一般都会小于100。这时就需要AtomicInteger原子类。AtomicInteger类中有许多方法,比如我们熟悉的compareAndSet(int expect, int update)、getAndIncrement()、 incrementAndGet()

 

unsafe类:

从源码可以发现getAndIncrement()、 incrementAndGet()都是使用的unsafe.getAndAddInt()方法

 

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

 

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

 

  Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要用过本地方法(native)来访问,Unsafe相当于一个后门,基于类可以直接操作特定内存的数据。Unsafe类位于sun.misc包中,其内部方法操作可以像C的指针一样操作内存,因为CAS操作是执行依赖于Unsafe类的方法。Unsafe的所有方法都是Native修饰就是直接调用操作系统底层资源执行相应的任务

getAndAddInt()方法:

再往底层翻源码就会看到getAndAddInt()方法其实用的就是CAS(compareAndSwap)

  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;
    }

 从incrementAndGet()方法中可以看出getAndAddInt()方法中

var1就是当前数据,

var2就是valueOffset就是内存偏移量,就是当前数据在内存地址。

var4就是固定值1,因为这是i++操作。

从getAndAddInt()方法源码开一看到用的是do while,首先就是获取当前数据在内存中的实际值var5,然后在while中又从内存中拿到数据,把这个数据和在do中拿到的var5比较是否相等

  如果相等执行var5+var4也就是i++,同时相等的话this.compareAndSwapInt(var1,var2,var5,var5+var4)是true,前面取反,整个while也就是false,跳出循环return var5

  如果不等,继续do while,从内存中拿值,比较直到相等,这也就是自旋,所以CAS也叫自旋锁。

getAndIncrement()、 incrementAndGet()区别:

这个很容易发现上面的源码截图中也很明显,incrementAndGet()是+1之后返回,getAndIncrement()是返回原来的值。

AtomicInteger atomicInteger = new AtomicInteger(0);
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.incrementAndGet());

结果:
0
2

Process finished with exit code 0

可以看出初始值是0,getAndIncrement()方法虽然+1了,但是返回的值是原来的值0,但是现在值现在就是1,incrementAndGet()方法+1之后返回的是2,也就是说返回的是操作之后的结果值。

compareAndSet()方法:

    /**
     * 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);
    }

这个方法也很好理解,我们在使用的时候传了expect和update两个值就是期望值和更新值,然后compareAndSwapInt()方法底层原理也讲过了,意思就是去主存里面看看这个值和快照的expect我们的期望值是不是一样,当然如果不一样肯定是因为这个值被动了,我们期望他是一样的,这样就能把expect值写入到主存中。

CAS缺点:

1、cpu开销大,在高并发下,许多线程,更新一变量,多次更新不成功,循环反复,给cpu带来大量压力。
2、只是一个变量的原子性操作,不能保证代码块的原子性。
3、ABA问题,就算这个值和我们期望的值一样,就一定能说明这个值没有被动过吗,显然不一定,有可能是从A改到B然后又改回A了。

举个ABA问题例子:内存值V=100;
threadA 将100,改为50;
threadB 将100,改为50;
threadC 将50,改为100;

场景:取款,由于机器不太好使,多点了几次取款操作。后台threadA和threadB工作,
此时threadA操作成功(100->50),threadB阻塞。正好有人给打款50元(50->100),
threadC执行成功,之后threadB运行了,又改为(100->50)。很明显这个逻辑不对。如何解决aba问题:

对内存中的值加个版本号,在比较的时候除了比较值还的比较版本号。

AtomicStampedReference就是用版本号实现cas机制。

posted @ 2020-07-30 20:46  卯仙  阅读(212)  评论(0编辑  收藏  举报