CAS

CAS的概念

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

以上内容取自于维基百科,从描述的最后一句话我们就可以看出CAS的思路。既然要做比较,那符号两边是不是分别要两个值,那比较完之后如果需要替换是不是要需要一个值。所以CAS存在三个参数,分别是 V, Expected NewValue。V 就是原来内存中的值(要改的那个值),Expected 就是指定数据(期望的值),NewValue 就是新的值(最后结果的值)。用代码表示如下所示:

cas(V,Expected,NewValue){
    
  if(V == Expected){
      V == NewValue;
  }
  //retry or stop
}

为什么需要CAS

synchronized 采用独占的方式来获取共享变量,当有其他线程希望获取共享变量时只能挂起等待,所以synchronized 属于悲观锁,而悲观锁所带来的的线程获取锁或释放锁都会引起性能问题;而CAS操作则是当多个线程尝试使用CAS操作共享变量时,每一个线程并不会因为有其他线程正在操作该共享变量而挂起,因此CAS操作属于乐观锁,但最后只有其中一个线程能够操作成功,其他线程都会失败,具体后续是重试还是停止就由各自决定。简单点说,就是提升了效率。

悲观锁和乐观锁都是一种思维方式,而 Synchronized 和 CAS 则是其对应的体现。

CAS缺点

那CAS有没有什么缺点呢?但凡任何事物都存在两面性,CAS也不例外。接下来看看CAS的不足之处。

ABA问题

CAS最著名的问题就是ABA,那什么是ABA呢,我们假设 内存中的值是A,这个值的变化经历过A-->B-->A,最后在比较检查的时候是还是能够通过,但实质上内存中的值已经经历过变化了,这就是ABA问题。我们来举个栗子。

  • 线程A和线程B同时获取到要修改的值Z,线程A和线程B使用CAS操作时参数内存中的值和预期的值都是Z;
  • CPU调度,线程A成功修改值Z为X;
  • 此时又进来个线程C,他获取到线程A修改完的值X,线程C使用CAS操作时参数内存中的值和预期的值都是X;
  • CPU调度,可能线程C的操作优于线程B,线程C先进行操作把值从X又修改为了Z;
  • 终于轮到线程B了,线程B操作把Z修改为了Y,也操作成功;

但这中间值Z已经被修改过了,线程B是不知道的,还是傻傻的进行了修改。所以ABA问题是存在一定风险的。那ABA问题怎么解决呢,可以增加一个版本号(Version)作为比较检查的依据,也就是说不光要对比内存中的值和预期的值,还需要比较版本号,每次CAS操作成功之后将版本号(Version + 1)。还是同样的栗子。

  • 线程A和线程B同时获取到要修改的值Z,线程A和线程B使用CAS操作时参数内存中的值和预期的值都是Z,版本号也同为D;
  • CPU调度,线程A成功修改值Z为X,并把版本号D+1,变为D1;
  • 此时又进来个线程C,他获取到线程A修改完的值X,线程C使用CAS操作时参数内存中的值和预期的值都是X,版本号为D1;
  • CPU调度,可能线程C的操作优于线程B,线程C先进行操作把值从X又修改为了Z,并把版本号D1+1,变为D2;
  • 终于轮到线程B了,线程B操作把Z修改为Y,但此时由于当时获取的版本号D不等于现在的版本号D2,所以操作失败。

    循环次数过长

    如果存在大量线程线程A,B,C,D...到线程Z,都对同一个共享变量进行操作,同一时刻只有一个线程能够完成操作,那其他线程如果失败之后继续尝试,长时间的CAS重试操作,则会给CPU带来比较大的开销。所以就会对自旋的次数进行限制。比如 synchronized锁升级的时候,多次尝试CAS(自旋)始终未拿到锁,就会升级为重量级锁。

    Java中CAS的体现(1.8)

    AtomicXXX

    Java中CAS体现最出名的便是java.util.concurrent.atomic 包下的Atomic为开头的类了,我们这里以AtomicLong为栗子🌰。
    以最常使用的incrementAndGet() 为切入点,我们来看看具体是怎么实现的。

    public class AtomicLong extends Number implements java.io.Serializable {
      
      private static final Unsafe unsafe = Unsafe.getUnsafe();
      private static final long valueOffset;
     
      static {
          try {
              valueOffset = unsafe.objectFieldOffset
                  (AtomicLong.class.getDeclaredField("value"));
          } catch (Exception ex) { throw new Error(ex); }
      }
      
      private volatile long value;
    
      public final long incrementAndGet() {
          return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
      }
    }

    从源代码可以看出,incrementAndGet是调用了 Unsafe 类的getAndAddLong 方法,并传入了三个参数,this (当前对象),valueOffset1L

  • 这个valueOffset是变量值在内存中的偏移地址,在静态代码块中完成该值的获取, Unsafe就是通过偏移地址来得到数据的原值的;
  • value是volatile的,这样是为了保证value在多个线程之间读取到的是一样;
  • 这里最后要加1L 是因为UnsafegetAndAddLong方法返回的是旧值 语义是i++ ;而incrementAndGet 的语义是++i

看来问题的关键还是在Unsafe 类,先简单介绍下Unsafe 类。

Java是无法直接访问操作系统的,而是通过native方法来进行访问。Unsafe类就是在Java层面提供了这样的一个支持。native方法多是调用C或着C++的代码,然后C或者C++代码再调用汇编语言生成一些CPU的指令来操作。
public final class Unsafe {    
    public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
}

getAndAddLong方法先根据传进来的当前对象和值在内存中的偏移地址得到旧值(期望值)。然后调用native方法compareAndSwapLong(cas操作),其中传入了四个参数:分别是当前对象,值在内存中偏移地址,期望值,新值。当成功时退出循环,并返回该旧值。这里的native方法compareAndSwapLong,就是调用了C或者C++的代码,最后生成了一条CPU指令cmpxchg保证了其操作的原子性。
我们刚才提到了CAS会遇到ABA问题,Java中也提供了相应的类AtomicStampedReferenceAtomicMarkableReference作为解决方案。本质上也是通过版本号,有兴趣的小伙伴可以从参考链接 死磕Java并发 和 小家Java中仔细阅读其使用和原理。

LongAdder

LongAdder 在JDK1.8中推出,下面的JavaDoc是对其的作用描述与AtomicLong 的比较。

* <p>This class is usually preferable to {@link AtomicLong} when
 * multiple threads update a common sum that is used for purposes such
 * as collecting statistics, not for fine-grained synchronization
 * control.  Under low update contention, the two classes have similar
 * characteristics. But under high contention, expected throughput of
 * this class is significantly higher, at the expense of higher space
 * consumption.

从描述中就可以看出,LongAdder更多地用于收集统计数据,而不是细粒度的同步控制。LongAdderAtomicLong 在低并发时不会有较大区别,但在高并发时,LongAdder 通过牺牲空间了来提高了吞吐量。
简单介绍下LongAdder 的设计思路,LongAdder 会把值放到一个数组里作为数组元素的值,然后将并发线程分散到其中的一个值去进行计算,最后将这些值累加起来就是最后的值。
image.png
该图取自【小家java】AtomicLong可以抛弃了,请使用LongAdder代替(或使用LongAccumulator)
接下来看看源代码,我们以increment 为切入点来看看。

public class LongAdder extends Striped64 implements Serializable {
    ...
    public void increment() {
        add(1L);
    }
    
     public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }   
}
abstract class Striped64 extends Number {
     /**
     * Base value, used mainly when there is no contention, but also as
     * a fallback during table initialization races. Updated via CAS.
     */
    transient volatile long base;
    
    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
}

从代码第9行可以看出,Cell数组是进行了判空操作,说明这个数组不是一开始就初始化好的,那后面casBase(b = base, b + x)就是在进行cas操作,这个base的值是LongAdder 父类Striped64中定义的,从注释信息上来看是基值,当线程无争用时使用,看到这里就可以得出一个结论:LongAdder 一开始线程竞争无竞争时并不会直接使用Cell数组,而是使用volatile的base变量来存储。uncontended 表示竞争是否激烈,接下来的代码就是当上一个条件casBase 失败表示出现竞争了,下面的判断条件就是竞争刚出现还没创建Cell数组或线程所在Cell的cas操作失败表示竞争很激烈,这时候就需要调用longAccumulate 方法进行Cell数组的创建和扩容。
最后当你需要获得值时需要调用sum方法,就是把Cell数组的值加起来得到最后结果。

public class LongAdder extends Striped64 implements Serializable {

    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }
}

参考链接

乐观锁的一种实现方式——CAS
【死磕Java并发】—-深入分析CAS
【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了
【小家java】AtomicLong可以抛弃了,请使用LongAdder代替(或使用LongAccumulator)


Ekko
2 声望0 粉丝