Fork me on GitHub

CAS介绍

概述

  cas即(compare and swap),比较并交换,在java并发中使用非常广泛,无论是ReenterLock内部的AQS,还是各种Atomic开头的原子类,都是基于cas实现的,java8的ConcurrentHashMap也使用了cas + synchronized进行实现,本文就介绍一下cas的原理。

cas原理

在CAS中有三个参数:内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干。以上过程整个是一个原子操作。

硬件底层实现原子性的方法:

  1. 总线锁定:当某个CPU需要修改某个数据的时候,通过锁住内存总线,使得别的CPU无法访问内存中的数据,从而保证缓存的一致性,但这种实现方式会导致CPU执行效率降低,现在很少被使用。
  2. 缓存锁:当一个CPU要修改缓存中的变量时,会对缓存加锁,同时会通过总线通知别的CPU,让他们的变量副本失效,这样同样可以保证一次只有一个CPU修改变量的值,从而保证缓存一致性。

AtomicInteger如何使用cas保证原子性

下面分析一下AtomicInteger源码

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:这个变量的作用是为了记录value的内存地址的,在AtomicInteger初始化的时候,会执行static{}这段静态代码块,之后会给这个变量赋值,通过unsafe的objectFieldOffset方法获取value的地址,有了这个内存地址,就可以使用CAS对value进行原子操作。

Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作。

下面来看下AtomicInteger的#addAndGet方法

 public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

进入#unsafe.getAndAddInt()

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

这里有一个while循环会一致尝试,直到cas成功,这就是cas的一个缺点,忙等,下面还会介绍。

看一下#compareAndSwapInt(),这个就是实现CAS操作的方法,是一个native方法

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

参数的含义:当前对象、value的地址、预期值、修改值

下面用一个图演示一下上面代码的过程

       图片来源:Java魔法类:Unsafe应用解析

上图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress=“0x110000”,通过baseAddress+valueOffset得到value的内存地址valueAddress=“0x11000c”;然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。

CAS的缺点

循环时间太长

如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。

ABA问题

CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。

Java中提供了AtomicStampedReference来解决,AtomicStampedReference通过包装一个元组[E,Integer],其中第一个元素就是要修改的元素,第二个就是为了记录版本信息,使用一个类来封装这两个变量信息,类源码如下:

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

该类是AtomicStampedReference中的一个静态内部类,第一个参数reference就是要修改的变量信息,第二个stamp就是版本信息,而且这两个变量都被final修饰,也就是说这个对象一旦初始化,这两个对象的值就确定了,不可以更改,那也就是说每次执行CAS成功,都要重新创建一个新的Pair对象。

下面看一下AtomicStampedReference的#compareAndSet方法

  public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

参数解释:预期引用(cas要修改的原值)、更新后的引用(cas修改之后的值)、预期标志(版本)、更新后的标志(版本)。后面是一个return,这个里面的代码比较长,前面两个判断是为了看一下现在系统中的值和版本是不是和自己期望的一致,如果一致,进行下一个判断,如果更新后的值和版本和系统当前的值和版本一致,直接返回,意思就是没有必要执行CAS操作,因为自己更新后的和更新前的版本和值都是一样,传入的参数有问题,如果不一样,则会进行CAS更新,会重新新建一个Pair对象,Pair.of这个方法在上面Pair源码中有贴出,大家参考着看就行了。

总结

本文介绍CAS的原理,以及分析了Java中的AtomicInteger对象是如何使用CAS进行原子操作的,之后分析了CAS存在的一些问题,以及这些问题的解决办法,总的来说CAS并不复杂,只是需要调用Unsafe,其实这个魔法类还是挺复杂的。

 

参考:

Java魔法类:Unsafe应用解析

【死磕Java并发】—-深入分析CAS

面试必问的CAS,你懂了吗?

 

posted @ 2020-09-03 15:54  猿起缘灭  阅读(999)  评论(0编辑  收藏  举报