1 引言

我们在日常的开发中,经常会使用 i++, 但是在并发环境下 i++是非线程安全的,此时我们可以使用 AtomicInteger 来保证线程安全性。

Atomic原子类底层核心的原理就是 CAS(Compare and Set),每次尝试修改的时候,就对比一下,有没有人修改过这个值,没有人修改,自己就修改,如果有人修改过,就重新查出来最新的值,再次重复这个过程。

2 先聊聊 Unsafe 类

Unsafe类是在 JDK 底层的一个类,底层限制不允许开发者直接实例化以及使用里面的方法。

private Unsafe() {
  
}

构造函数是私有的,不可以直接进行实例化,其次,如果用Unsafe.getUnsafe()方法来获取一个实例是不行的,它会判断一下,如果当前是属于我们的用户的应用系统,识别到有用户的类加载器以后,就会报错,不让获取实例。

当然,我们其实可以使用反射来改变访问权限。

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

Unsafe 类是JDK自己内部使用的,不是对外的。

Unsafe,封装了一些不安全的操作,指针相关的一些操作,就是比较底层了,Atomic原子类底层大量的运用了Unsafe。

JUC下面大量使用了CAS操作,它们的底层是调用的Unsafe的 CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。

3 核心属性

AtomicInteger 核心属性主要包括:

  • unsafe 实例
  • valueOffset:标识value字段的偏移量
  • value:存储int类型值的地方,使用volatile修饰
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;

Unsafe 类中使用静态代码块来初始化 valueOffset,valueOffset 表示 value 这个字段具体是在 AtomicInteger 这个类的哪个位置,offset 即偏移量,底层是通过unsafe来实现的。在类初始化的时候,就会完成这个操作,一旦初始化完毕,就不会再变更了。

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

4 核心方法

4.1 compareAndSet()方法

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// Unsafe 类中的方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

调用Unsafe.compareAndSwapInt()方法实现,这个方法有四个参数:

(1)操作的对象;

(2)对象中字段的偏移量;

(3)原来的值,即期望的值;

(4)要修改的值;

可以看到,这是一个native方法,底层是使用C/C++写的,主要是调用CPU的CAS指令来实现,它能够保证只有当对应偏移量处的字段值是期望值时才更新,即类似下面这样的两步操作:

if(value == expect) {
    value = newValue;
}

通过CPU的CAS指令可以保证这两步操作是一个整体,也就不会出现多线程环境中可能比较的时候value值是a,而到真正赋值的时候value值可能已经变成b了的问题。

4.2 getAndIncrement()方法

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe 类中的方法
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;
}

getAndIncrement()方法底层是调用的Unsafe的getAndAddInt()方法,这个方法有三个参数:

(1)操作的对象;

(2)对象中字段的偏移量;

(3)要增加的值;

查看Unsafe的getAndAddInt()方法的源码,可以看到它是先获取当前的值,然后再调用compareAndSwapInt()尝试更新对应偏移量处的值,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止,这可不就是(CAS+自旋)的乐观锁机制么^^

AtomicInteger中的其它方法几乎都是类似的,最终会调用到Unsafe的compareAndSwapInt()来保证对value值更新的原子性。

5 CAS 的缺陷

5.1 ABA 问题

如果某个值一开始是A,后来变成了B,然后又变成了A,你本来期望的是值如果是第一个A才会设置新值,结果第二个A一比较也符合条件,也设置了新值,这跟期望是不符合的。所以atomic包里有 AtomicStampedReference 类,就是会比较两个值的引用是否一致,如果一致,才会设置新值。

举个例子:假设一开始变量i = 1,你先获取这个i的值是1,然后累加了1,变成了2。但是在此期间,别的线程将i -> 1 -> 2 -> 3 -> 1。这个期间,这个值是被人改过的,只不过最后将这个值改成了跟你最早看到的值一样的值。结果你后来去compareAndSet的时候,会发现这个i还是1,就将它设置成了2,就设置成功了。

不过 AtomicInteger 常见使用场景的是计数,所以说一般是不断累加的,所以ABA问题比较少见

5.2 无限循环问题

大家看源码就知道Atomic类设置值的时候会进入一个无限循环,只要不成功,就不停循环再次尝试,这个在高并发修改一个值的时候其实挺常见的,比如你用AtomicInteger在内存里搞一个原子变量,然后高并发下,多线程频繁修改,其实可能会导致这个compareAndSet()里要循环N次才设置成功,所以还是要考虑到的。

JDK 1.8引入的LongAdder来解决,采用了分段 CAS 思路。

5.3 多变量原子问题

一般的AtomicInteger,只能保证一个变量的原子性,但是如果多个变量呢?你可以用 AtomicReference,这个是封装自定义对象的,多个变量可以放一个自定义对象里,然后他会检查这个对象的引用是不是一个。