一年工作经验的我,居然被问到了CAS

楔子

前一篇文章,我们介绍了 synchronized,知道了 synchronized 可以解决某些数据的原子性问题,本篇文章我们以 AtomicInteger 为切入点,继续学习 CAS 无锁化的知识。

使用 synchronized 解决 i++的原子性

使用 synchronized 关键字,保证数据原子性

public class Test {

   static int synchronizedValue = 0;
   
   public static void main(String[] args) {
      // 使用 synchronized 的++
      synchronizedAdd();
   }
   
   private static void synchronizedAdd() {
      for(int i = 0; i < 50; i++) {
         new Thread(() -> {
            synchronized(Test.class) {
               System.out.println("synchronizedAdd:"+ ++Test.synchronizedValue);;
            }
         }).start();
      }
   }
    
}

结果:

synchronizedAdd:50

使用 AtomicInteger 解决自增原子性

static AtomicInteger atomicValue = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
   // 使用 atomic 的++
   atomicAdd();
}
private static void atomicAdd() {
   for(int i = 0; i < 50; i++) {
      new Thread(() -> System.out.println("atomicAdd:"+ Test.atomicValue.incrementAndGet())).start();
   }
}

结果

atomicAdd:50

CAS 原理

什么叫 CAS 呢?英文全称 compare and swap,翻译成中文就是比较和交换的意思。专业的说法叫乐观锁。说人话就是,我们修改数据的时候,会去尝试比较一下,这个值有没有被其他人修改过,如果没有被修改,那么我们自己就可以修改;如果已经被修改过了,那么我们就重新获取最新的值,重复执行以上步骤再次去比较。接下来我们从 AtomicInteger 源码进一步分析CAS。

AtomicInteger 源码解析

观察 AtomicInteger 源码,我们发现,他可以分为几个部分。

  1. Unsafe:核心类,真正负责执行 CAS 操作的类
  2. value 和 valueOffset:值与偏移量
  3. API 接口:对外提供各种使用方式,主要是封装了 Unsafe 的一些操作

Unsafe

Unsafe 顾名思义,这是一个不安全的类,内部是大量的 native 方法,JDK 原则上是不允许我们使用他的,他是供给 JDK 内部使用的一个类。首先他的构造函数是私有化,不能自己手动去实例化他, 其次,虽然他提供了 Unsafe.getUnsafe()方法来获取一个实例,但是他有一个判断,如果他不是由系统加载的他就会直接抛出异常,以上提到的源码如下:

// (1)构造方法私有化 
private Unsafe() {
}

public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // (2)如果 Unsafe 不是由 JVM 加载的,那么就会报错
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

valueOffset

类初始化的时候,会加载一个静态代码块,通过 unsafe 去确定一个 final 标记的偏移量,源码如下

private static final long valueOffset;

// 类初始化的时候,就会执行该静态代码块,通过 unsafe 去确定一个 final 标记的偏移量
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

接着我们回到我们自己写的方法 atomicAdd

private static void atomicAdd() {
    for(int i = 0; i < 50; i++) {
        new Thread(() -> System.out.println("atomicAdd:"+ Test.atomicValue.incrementAndGet())).start();
    }
}

跟进 incrementAndGet 方法,这是一个包装方法,直接调用的 unsafe.getAndAddInt

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

跟进 unsafe.getAndAddInt 方法,好了以下,就是我们需要分析的代码了

public final int getAndAddInt(Object 当前对象, long 偏移量, int 自增值) {
    int 对象的 value;
    do {
        // 这个本地方法的作用是,
        // 从 AtomicInteger 对象实例,根据 valueOffset 偏移量,获取 value 这个字段的位置,从而获取到当前的 value 的值
        对象的 value/计算出的对象的 value = this.getIntVolatile(当前对象, 偏移量);
    } 
    // 如果【计算出的 value】和【当前对象+偏移量】的不一致,那么这一次的 compare 就为 false,那么就会进入下一次循环
    // 如果【计算出的 value】和【当前对象+偏移量】的一致,那么这一次的 compare 就为 true,此时
    // 【对象的 value】 = 【计算出的对象的 value】 + 【自增值】,并且跳出循环
    while(!this.compareAndSwapInt(当前对象, 偏移量, 计算出的对象的 value, 计算出的对象的 value + 自增值));

    return 对象的 value;
}

Atomic 原子类 CAS 常见的几种问题

ABA 问题

举个例子,比如当某个值为 A 的时候,你才进行操作。所以有可能会出现以下情况

  1. 初始为 A
  2. A——>B
  3. B——>A
  4. 开始操作
    (2)到(3)这个步骤,这个值是被人改过的,但是这个值和我期望的值是一样的,所以我们去 compareAndSwapInt 的时候,会发现这个值还是 A,就设置成功了。

注:如何解决 ABA 问题呢?加个时间戳就搞定了,比较的时候带上时间戳。

无限循环问题

看源码,因为我们是走的 do…while循环,所以有可能会循环很多次,都比较不成功。

注:如何解决无限循环问题呢?jdk 给我们提供了一个思路,分段 CAS,感兴趣的读者可以参考 LongAdder 类。

自定义对象原子问题

AtomicInteger,只能保证一个变量的原子性。

注:复杂对象怎么办呢?jdk 给我们提供了 AtomicReference,他比较的是这个对象的引用是不是一个。

上一篇:CAS(Compare And Set)


下一篇:兴达易控MPI转光纤模块