Java中CAS原理分析

CAS是什么?

在java中锁可以分为悲观锁和乐观锁,今天介绍的CAS就是属于乐观锁的一种实现方式。CAS全称为 Compare And Swap 即比较和交换。乐观锁的乐观的意思就是我认为自己肯定会拿到锁资源,如果拿不到我就一直尝试,直到拿到锁资源为止。

接下来以AtomicInteger原子类来详细解释说明:

private static int threadNum = 100;
private static int numberA = 0;
private static AtomicInteger numberB = new AtomicInteger(0);
private static final ExecutorService executorService = Executors.newCachedThreadPool();
public static void main(String[] args) {
  for (int i = 0; i < threadNum; i++) {
    executorService.execute(() -> {
      numberA += 1;
      numberB.getAndIncrement();
  });
}
System.out.println(numberA); // 95
System.out.println(numberB); // 100
}

通过上面代码可以看出在多线程场景下对AtomicInteger操作是线程安全的而对numberA变量的操作是线程不安全的。

首先进入getAndIncrement()方法

public final int getAndIncrement() {
  // 使用unsafe对象的getAndAddInt方法
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

查看unsafe对象的类:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
  try {
    // 获取value字段相对于当前对象的的offset(起始地址)
    valueOffset = unsafe.objectFieldOffset
    (AtomicInteger.class.getDeclaredField("value"));
  } catch (Exception ex) { throw new Error(ex); }
}

这里的静态代码块AtomicInteger对象初始化之前就执行了,获取AtomicInteger对象value字段相对AtomicInteger对象的”起始地址”的偏移量,Java对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),”起始地址”的偏移量即是对象头的偏移量。

紧接着继续看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;
}

于上述代码var1,var2等变量不见名知意,转化如下:

public final int getAndAddInt(Object obj, long valueOffset, int delta) {
    int expect;
    do {
        // 先获取内存offset出的value值
        expect = this.getIntVolatile(obj, valueOffset);
        // 然后通过compareAndSwapInt比较并且交换
        // delta为当前value需要加上的数值
        // update = expect + delta
    } while(!this.compareAndSwapInt(obj, valueOffset expect, update));
    return expect;
}

上述代码流程为:

  1. 先获取出当前对象obj(即AtomicInteger对象)在内存中valueOffset处的值
  2. 再通过compareAndSwapInt方法比较获取到的期望值expect与对象obj内的value值是否相等,若相等则更新成update值,否则再次进入循环,直到更新成功为止。

执行流程如下图:

Java中CAS原理分析_第1张图片
上面的步骤可以分为三步:

  1. 读取, 从内存中获取对象valueOffset处的value值
  2. 比较
  3. 修改

其中比较和修改要保证原子性

CAS底层靠调用CPU指令集cmpxchg来完成原子操作的。但在多核场景下该指令也不能保证原子性。需要在前面加上lock指令。lock指令可以保证一个CPU核心在操作期间独占一片内存区域。在多核处理中一般通过总线锁和缓存锁来实现。

  1. 总线锁: 多核处理器,CPU是通过总线访问内存的, 可以通过锁住总线来让其他核心无法访问内存
  2. 缓存锁: 总线锁的代价很大,会导致其他核心无法工作。缓存锁是锁住某部分的内存空间, 当一个CPU核心将内存读取到自己的缓存区后,就锁定对应内存区域,期间其他CPU核心无法操作这块区域。

总结

通过上面的AtomicInteger源码的阅读,我对CAS的理解:

CAS操作是不加锁的, 它每次都假设自己可以执行成功, 然后它就去尝试执行了,成功就返回,否则就一直循环去不断地尝试。这样做比加锁和释放锁的性能是要高的。但是如果一直都是失败的,这样长时间的循环(自旋)会给CPU带来很大的开销。

其次CAS还会有ABA的问题,比如我原来value的值为 A, 期间改成了B,后来有变成了A,这个时候某一个线程去执行CAS的之后就会认为value的值没有更改过。但是实际却变化过了。可以使用AtomicStampedReference类解决ABA问题。

还有就是CAS只能保证一个共享变量的原子操作, 没办法保证一块代码或者一个对象的原子操作。

你可能感兴趣的:(java,java,cas,atomic)