并发编程CAS & Atomic详解

191 阅读3分钟

CAS

在说CAS之前我们先看一个小例子:多线程下计数器的问题

private static int num = 0;
public static void main(String[] args) throws InterruptedException {
  //创建10个线程每个线程循环1000次,正常的话计算出来的sum应该是10000
  for (int i = 0; i < 10; i++) {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        for (int i1 = 0; i1 < 1000; i1++) {
          num++;
        }
      }
    });
    thread.start();
  }
  Thread.sleep(1000);
  System.out.println(num);
}

但是运行之后会发现num的值不固定例如:9627、9297等,那有什么方案可以实现最终结果是10000,那有的同学说可以使用synchronized 和加锁lock。

但是上面两个方案都会导致线程阻塞挂起,进而导致线程上下文切换和重新调度的开销,那有没有不加锁的方案也能实现呢?

CAS也是可以实现上面的要求,可以考虑使用AtomicInteger,当然自己也可以使用Unsafe实现简化版的AtomicInteger,Atomic这些底层也是通过CAS来实现的。那我们来看一下什么是CAS。

什么是CAS

CAS(Compare and Swap,比较并交换),通常如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect的一种非阻塞原子操作。java中通过UnSafe里面的compareAndSwapObject、compareAndSwapInt、compareAndSwapLong实现的。

CAS操作有4个参数

  • 对象内存中位置
  • 对象中变量的偏移量
  • 变量预期值
  • 新的值

CAS简单使用

public class CasSimple {
    private static Entry entry = new Entry();
    public static void main(String[] args) throws Exception {
        Unsafe unsafe = getUnsafe();

        long valueOffset = unsafe.objectFieldOffset(entry.getClass().getDeclaredField("num"));
        System.out.println(valueOffset);
        boolean result = unsafe.compareAndSwapInt(entry, valueOffset, 01);
        System.out.println("第一次cas操作结果:" + result +", num = " + entry.num);

        result = unsafe.compareAndSwapInt(entry, valueOffset, 13);
        System.out.println("第二次cas操作结果:" + result +", num = " + entry.num);

        result = unsafe.compareAndSwapInt(entry, valueOffset, 15);
        System.out.println("第三次cas操作结果:" + result +", num = " + entry.num);
    }
    private static Unsafe getUnsafe() throws Exception{
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(Unsafe.class);
    }
}
class Entry{
    int num = 0;
}

运行结果如下:

第一次cas操作结果:true, num = 1
第二次cas操作结果:true, num = 3
第三次cas操作结果:false, num = 3

CAS实现原理

由于是native方法,我们看一下jdk的源码看看是怎么实现的,这看的是openJDK的代码,找到unsafe.cpp文件

// offset 偏移量、e要比较的值、x期望设置的值
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  //根据偏移量计算出value的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  //CAS操作逻辑
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

核心逻辑在Atomic::cmpxchg方法中,这个是根据不同操作系统和不同的CPU有不同的试下,这里查看的是linux_x86中的实现文件在 atomic_linux_x86.inline.hpp

exchange_value最终要替换的值, dest表示某值对应的内存地址,compare_value要比较的值,

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  //判断当前执行环境是否为多处理器环境
  int mp = os::is_MP();
  //LOCK_IF_MP(%4)在多处理器环境下,为cmpxchgl指令添加lock前缀,以达到内存屏障的效果
  //cmpxchgl 是一种包含在x86架构及IA-64架构中的一个原子操作的汇编指令
  __asm__ volatile (LOCK_IF_MP(%4"cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc""memory");
  return exchange_value;
}

嵌入式汇编规定把输出和输入寄存器按统一顺序编号,顺序从输出开始从左往右 以 "%0"开始,

所以cmpxchgl %1,(%3) 实际上表示cmpxchgl exchange_value,(dest),cmpxchgl中有一个隐含操作数eax,实际过程是先比较eax对应的值(也就是compare_value)和dest进行比较, CAS指令作为一种硬件原语,有着天然的原子性

#第一个输出参数,把eax中的值写入到exchange_value中,方法结束后返回
%0:"=a" (exchange_value)  
#表示compare_value存入eax寄存器、exchange_value、dest、mp存入任意的通用寄存器
%1:"r" (exchange_value), 
%2:"a" (compare_value), 
%3:"r" (dest), 
%4:"r" (mp)

执行过程:

cmpxchgl执行时先判断cmpare_value和dest是否相等,如果相等则把eax对应的值指向exchange_value,把eax存的compare_value 赋值给echange_value, 这个时候实际返回的是compare_value对应的地址。 如果compare_value 和 dest不相等,则把dest的值写入到eax,然后在赋值给exchange_value进行返回

伪代码如下:

a = 10;//cmpare_value
b = 10;//dest
c = 12;//exchange_value
eax = a;
if(a == b){
   eax = c;
   c = a;
} else {
  eax = b;
  c = eax;
}
return c;

经过上面的分析大家是不是已经对CAS怎么使用和工作原理已经清楚了呢,那我们来总结一下CAS的优缺点

CAS 优缺点

优点

可以高效的解决原子操作

缺点

  • 自旋CAS长时间不成功则会给CPU带来非常大的开销

    java中原子类递增都是通过CAS自旋实现的,如下代码:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
            //如果长时间自旋失败则会给CPU带来较大的开销
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    }
    
  • 只能保证一个共享变量的原子操作

    就拿AtomicInteger举例,一次只能保证一个共享变量的原子性

  • ABA问题

说明一下第三个ABA缺陷,那什么是ABA问题呢?三个线程 A线程设置变量为10,B线程把变量改为20,C又把变量改回10,这个时候A去修改变量的时候还是成功的,但是实际上值已经有被改动过了如下图:

image-20220117174153893.png

通过代码验证一下

private static AtomicInteger atomicInteger = new AtomicInteger(10);
public static void main(String[] args) throws Exception {
  new Thread(() -> {
    int value = atomicInteger.get();
    System.out.println("ThreadA get value = " + value);

    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    if(atomicInteger.compareAndSet(value20)){
      System.out.println("ThreadA update "+value +" to 20");
    }else {
      System.out.println("ThreadA update fail!");
    }
  },"ThreadA").start();
  new Thread(() -> {
    int value = atomicInteger.get();
    System.out.println("ThreadB get value = " + value);

    if(atomicInteger.compareAndSet(value20)){
      System.out.println("ThreadB update "+value +" to 20");
    }
    value = atomicInteger.get();
    System.out.println("ThreadB get value = " + value);

    if(atomicInteger.compareAndSet(value10)){
      System.out.println("ThreadB update "+value +" to 10");
    }
  },"ThreadB").start();
}

运行结果如下:

ThreadA get value = 10
ThreadB get value = 10
ThreadB update 10 to 20
ThreadB get value20
ThreadB update 20 to 10
ThreadA update 10 to 20

可以看出 ThreadA 通过CAS操作还是可以设置成功的。

但是有的业务场景下ABA这种问题是要解决的。那怎么解决呢?可以使用java提供的AtomicStampedReference,看如下例子

//第一个变量为实际存储的变量,第二个变量为版本信息 每次修改则会+1
private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference(101);
public static void main(String[] args) throws Exception {
  new Thread(() -> {
    int[]stampHolder = new int[1];
    int value = atomicInteger.get(stampHolder);
    //可以使用stampHolder[0]表示版本,主要是因为 get()方法有这一句话  stampHolder[0] = pair.stamp;
    System.out.println("ThreadA get value = " + value+" 版本为 = " + stampHolder[0]);
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    if(atomicInteger.compareAndSet(value20,stampHolder[0],stampHolder[0]+1)){
      System.out.println("ThreadA update "+value +" to 20");
    }else {
      System.out.println("ThreadA update fail!");
    }
  },"ThreadA").start();
  new Thread(() -> {
    int[]stampHolder = new int[1];
    int value = atomicInteger.get(stampHolder);
    System.out.println("ThreadB get value = " + value+" 版本为 = " + stampHolder[0]);

    if(atomicInteger.compareAndSet(value20,stampHolder[0], stampHolder[0]+1)){
      System.out.println("ThreadB update "+value +" to 20");
    }
    value = atomicInteger.get(new int[1]);
    System.out.println("ThreadB get value = " + value+" 版本为 = " + stampHolder[0]);

    if(atomicInteger.compareAndSet(value10,stampHolder[0], stampHolder[0]+1)){
      System.out.println("ThreadB update "+value +" to 10");
    }
  },"ThreadB").start();
}

运行效果如下:

ThreadA get value = 10 版本为 = 1
ThreadB get value = 10 版本为 = 1
ThreadB update 10 to 20
ThreadB get value = 20 版本为 = 1
ThreadA update fail!

在使用过程中根据自己的场景选择相应的Atomic原子操作类。上面介绍了AtomicInteger 和AtomicStampedReference 那都有哪些Atomic原子操作类呢,下面跟着胖哥一块来看看吧。

Atomic原子操作

java使用CAS在JUC包下的atoimic包中提供了一些列的原子操作类

  • 基本类型:AtomicInteger、AtomicLong、AtomicBoolean
  • 引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference
  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • 原子类型累加器(1.8新增的) :DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64

这里就简单描述一下AtomicInteger,LongAdder下一篇在单独分析

AtomicInteger

那我们接下来看看AtomicInteger的属性和get、set方法

//Unsafe类上面已经说过,里面包含了几个CAS相关的native方法
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); }
}
//对应的值 加上volatile为了保证可见性
private volatile int value;

来看一下 getAndIncrement方法 自增方法,每次+1

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}
//Unsafe中的方法,
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
    //这个是native方法
    var5 = this.getIntVolatile(var1, var2);
    //执行CAS操作,如果失败则进行自旋操作 ①
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

  return var5;
}

考虑一下在高并发环境下 上面标注的①代码 是否有性能瓶颈呢?

在JDK1.8中引入了LongAdder其目的就是解决高并发环境下AtomicInteger, AtomicLong的自旋瓶颈问题。胖哥下一篇再来分析下LongAdder解决自旋瓶颈问题的。

总结

通过上面的例子和源码分析大家已经对CAS有一定的了解了,如果遇到相关的场景了就赶快用起来吧。

我是二胖,公众号:二胖学技术