CAS 原子编程底层源码分析

一、前言

1、原子类为了解决什么问题?
     为了解决并发场景下无锁的方式保证单一变量的数据一致性

2、什么情况下存在并发问题?
     多个线程同时读写同一个共享数据时存在多线程并发问题

二、非原子计算

大家应该都知道, 类似于代码中的 i++ 操作, 虽然是一行, 但是执行时候是分为三步的

  • 从主存获取变量 i
  • 变量i值+1
  • 新增后变量i值写回主存

写个小程序来更好的理解, 不论我们怎么运行下方程序,99% 以上概率不会到达 100000000

static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                NUM++;
            }
        }).start();
    }
    Thread.sleep(2000);
    System.out.println(NUM);
    /**
     * 99149419
     */
}

那么如何俩解决这个问题呢?

1.可以使用 JDK 自带的synchronized, 通过互斥锁的方式同步执行 NUM++ 这个代码块

static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
 synchronized (Object.class) {
                    NUM++;
                }
            }
        }).start();
    }
    Thread.sleep(2000);
    System.out.println(NUM);
       /**
        *   100000000
        */
  }

2.如果不使用锁来解决上面的非原子自增问题, 可以这么来写 Atomic 开头的类库

static AtomicInteger NUM = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                  // 🚩 重点哦, 自增并获取新值
 NUM.incrementAndGet();
            }
        }).start();
    }
    Thread.sleep(2000);
    System.out.println(NUM);
    /**
     *100000000
     */
}

三、AtomicInteger

1、是什么?
  • AtomicInteger 是 JDK 并发包下提供的操作 Integer 类型原子类, 通过调用底层Unsafe 的 CAS 相关方法实现原子操作
  • 基于乐观锁的思想实现的一种无锁化原子操作,保障了多线程情况下单一变量的线程安全问题
2、有什么优点?
  • 尽管 synchronized 经过升级后, 性能有了大幅度提升, 但在一般并发场景下, CAS 无锁算法性能更高一些

  • AtmoicInteger使用硬件级别的指令 CAS 来更新计数器的值, 机器直接支持的指令,这样可以避免加锁,比如像互斥锁 synchronized 在并发比较严重情况下, 会将锁升级到重量级锁,唤醒与阻塞线程时会有一个用户态到内核态的一个转变, 而转换状态是需要消耗很多时间的

四、结构分析

  • AtomicInteger 有两个构造方法, 分别是一个无参构造及有参构造

无参构造的 value 就是 int 的默认值 0
有参构造会将 value 赋值

public AtomicInteger() { }

public AtomicInteger(int initialValue) {

    value = initialValue;

}
AtomicInteger 有三个重要的变量, 分别是:
  • Unsafe:可以理解它对于 Java 而言, 是一个 "BUG" 的存在,在 AtomicInteger 里的最大作用就是直接操作内存进行值替换
  • value:使用 int 类型存储 AtomicInteger 计算的值,通过 volatile 进行修饰,提供了内存可见性及防止指令重排序
  • valueOffset:value 的内存偏移量
// 获取Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

// 静态代码块,在类加载时运行
static {
    try {
          // 获取 value 的内存偏移量
valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {
        throw new Error(ex);
    }
}
private volatile int value;

常用API:

// 获取当前 value 值
public final int get();
// 取当前的值, 并设置新的值
public final int getAndSet(int newValue);
// 获取当前的值, 并加上预期的值
public final int getAndAdd(int delta);
// 获取当前值, 并进行自增1
public final int getAndIncrement();
// 获取当前值, 并进行自减1
public final int getAndDecrement();
我们拿getAndIncrement()来举一个例子看一下,其他的大同小异
public final int getAndIncrement() {
      return unsafe.getAndAddInt(this, valueOffset, 1);
}

/**
* unsafe.getAndAddInt
*
* @param var1 AtomicInteger 对象
* @param var2 value 内存偏移量
* @param var4 增加的值, 比如在原有值上 + 1
* @return
*/

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 内存中 value 最新值
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  
  return var5;
}

我们来看一下底层的执行步骤:

1.、根据 AtomicInteger 对象 以及 value 内存偏移量获取对应 value 最新值
2、通过 compareAndSwapInt(...) 将内存中的值(var5)更改为期望的值(var5+var4), 不存在多线程竞争成功修改返回 True 结束循环, Flase 继续执行循环

compareAndSwapInt(....)源码解析:
/**
* 比较 var1 的 var2 内存偏移量处的值是否和 var4 相等, 相等则更新为 var5
*
* @param var1 AtomicInteger 对象
* @param var2 value 内存偏移量
* @param var4 value 原本的值
* @param var5 期望将 value 设置的值
* @return
*/

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
由于是 native 关键字修饰, 我们无法查看其源码, 说明一下方法思路

1、通过 var1(AtomicInteger) 获取到 var2 (内存偏移量) 的 value 值
2、将 value(内存中值) 与 var4(线程内获取的value值) 进行比较
3、如果相等将 var5(期望值) 设置为内存中新的 value 并返回 True
4、不相等返回 False 继续尝试执行循环

五、不足之处

  • CAS 虽然能够实现无锁编程, 在一般情况下对性能做出了提升, 但是并不是没有局限性或缺点
  • 在高并发情况下, 自旋 CAS 如果长时间不成功, 会给 CPU 带来非常大的执行开销
  • CAS 需要在操作值的时候检查下值有没有发生变化, 如果没有发生变化则更新
  • 但是如果一个值原来是A, 变成了B, 又变成了A, 那么使用 CAS 进行检查时会发现它的值没有发生变化, 但是实际上却变化了
    如果感兴趣的小伙伴可以去看下 JUCA 原子包下的 AtomicStampedReference

六、结束

博主很懒!今天就先到这把,如何规避ABA问题等,下回分解把,其实也很简单,先来说下解决 ABA 的思路吧, 也就是 AtomicStampedReference 的原理:内部维护了一个 Pair 对象, 存储了 value 值和一个版本号, 每次更新除了 value 值还会更新版本号

有引用其他大佬们的一些东西,如有侵权请联系本人
作者:作者很懒不想打字

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容