JUC-AtomicXXX分析

950 阅读8分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

JUC-AtomicXXX分析

Atomic系列API除了提供了Integer,Long,Boolean这三类“基本基本数据类型”的API,还提供了对于对象引用的非阻塞原子性读写操作的AtomicReference。

AtomicBoolean & AtomicLong

对于AtomicBoolean而言,底层实现是通过int类型的value对0进行判断得到true or false。

而AtomicLong类型,和AtomicInteger很相似,区别在于,因为JVM底层对于int和long类型所占用的字节数是不一样的(int 4字节,long 8字节),而不同位数的CPU对cmpxchg指令的支持是不同的,比如4字节32位的CPU无法支持8字节64位的cmpxchg指令,那么此时就需要判断当前JVM版本是否支持8字节数字的cmpxchg操作;如果机器硬件与当前JVM的版本都不支持,那么实际上针对long型数据的原子性操作因为需要拆分成高4位和低4位两步进行,无法通过指令的lock前缀实现原子性,那么就需要采用加锁的方式确保原子性。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetLong(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jlong e, jlong x)) {
  Handle p(THREAD, JNIHandles::resolve(obj));
  jlong* addr = (jlong*)index_oop_from_field_offset_long(p(), offset);
// 如果机器硬件支持8位cmpxchg操作,直接干就完了
#ifdef SUPPORTS_NATIVE_CX8
  return (jlong)(Atomic::cmpxchg(x, addr, e)) == e;
#else
// 如果虚拟机版本支持了8位操作,同样直接调用cmpxchg
// 虚拟机这一层会实现8位的原子操作
  if (VM_Version::supports_cx8()) {
    return (jlong)(Atomic::cmpxchg(x, addr, e)) == e;
  } else {
    // 而如果硬件和虚拟机都不支持,那么就只能通过Mutex加锁实现了
    // 这里MutexLockerEx是一种简化操作的锁,mu销毁的时候,会自动释放锁。
    // 方法执行完毕,自行释放锁。
    MutexLockerEx mu(UnsafeJlong_lock, Mutex::_no_safepoint_check_flag);
    // 先加载内存值
    jlong val = Atomic::load(addr);
    // 如果预期值和内存值不等,修改失败
    if (val != e) {
      return false;
    }
    // 否则可以修改,调用store将新值写入
    Atomic::store(x, addr);
    return true;
  }
#endif
} UNSAFE_END

对于上面的SUPPORTS_NATIVE_CX8以及VM_Version::supports_cx8()的值,实际上在虚拟机的编译时就已经知道了对应的平台是否支持。

AtomicReference:

一个对象的引用,实际上就是一个4字节的数字而已,如下面的代码:

String a = "AAA";

对一个4字节存储的数值的load和store操作,本身就是原子性的操作,但是像上面的代码,却并不是原子性操作,实际上分成了两个部分,读取"AAA"这个字符串的地址,将这个地址写入到引用a所指代的4字节内存中。

AtomicReference就是为了解决上述行为非原子性操作的问题,因为对于一个引用的操作,只有两种行为,读取这个引用的值和更改这个引用所指向的地址。

和AtomicInteger一样,要保证一个对象引用操作的原子性,volatile配合synchronize或者Lock,也可以实现,原子类的意义仍然在于更高效便捷,而且这些类的关键API,都是compareAndSet系列。

而对于AtomicReference的comparaAndSet,较新版本的实现不再是基于Unsafe,而是换成了VarHandle。

因为 Unsafe 所操作的并不属于Java标准,会容易带来一些安全性的问题。JDK9 之后,官方推荐使用 java.lang.invoke.VarHandle 来替代 Unsafe 大部分功能,对比 Unsafe ,Varhandle 有着相似的功能,但会更加安全,且并发方面也提高了不少性能(前面的分析有说道一些调用UnSafe API的地方做了自旋操作,虽然是为了保证数据修改的成功,但是依然有一定的性能问题),虽然目前为止,AtomicInteger仍然是用Unsafe实现,但是随着版本的迭代,应该也会逐步替换成VarHandle系列API。

这里并未找到VarHandle的native method的具体实现源码,但是底层的实现推测依然是没有太大变化的,应该都是基于CPU的cmpxchg系列指令实现。

AtomicStampedReference:

通过前面几个原子类的分析,我们大体知道这类API的套路,通过volatile关键字保证数据在多线程之前的立即可见性,通过CAS确保数据修改的原子性,通过自旋确保修改必须成功。

ABA问题:

但是众所周知,CAS存在一个很明显的漏洞:ABA问题,即CAS无法感知,在修改的过程中,所要修改的内存值到底有没有发生A->B->A的变化,因为CAS在最终执行修改的时候,发现内存值仍然是A,就会认为没有产生变化,然后执行修改。

普通数值的场景下,ABA的问题并不会有什么问题,但是如果是复杂对象的引用场景,很容易导致引用关系产生错误。

如下图来说明这样一个问题:

假设我们有一个链表栈,对这个栈的入栈出栈操作,即Top指针的修改是CAS性质的操作,即如果要将一个元素B从链表栈中移除,他必须是Top指针指向B,oldV=B,才会修改成功,否则失败。

  1. 有一个链表栈,先后入元素A、B
  2. 线程1将要出栈B(即修改栈顶指针top指向B.next),但是线程1读取到了B.next(A)为ExpV,并未完成将top指向A的操作,此时栈顶Top指针指向B,oldV为B。
  3. 此时线程1挂起,线程2进入,直接完成了A、B的出栈。
  4. 此时线程1仍然挂起,线程2完成了C、D入栈后,然后又重新入栈了B,此时OldV仍然是B,而Top也是B。
  5. 此时线程1重新获得CPU执行时间,Top和OldV都是B,可以执行Top指向ExpV=A的操作。
  6. 最终,栈顶Top指针指向了A,而B、D、C则被丢掉了,因为A和BDC并没有引用链。

为Atomic加上Version:

ABA的问题,可以通过增加一个版本号的方式来解决,即修改一次引用,我们就给版本号递增,后面再执行CAS修改的时候,加上一个版本号的校验,只有当版本号和预期的一致且OldV和内存值一致,才会执行修改操作。

JUC有两个方式实现了类似的方案:

  1. AtomicStampedReference

AtomicStampedReference将版本号Stamp和Value封装成了一个Pair,而Stamp的递增,需要使用者自行维护,换言之,我们也可以通过给AtomicReference套一层的方式,实现类似的效果。

其核心compareAndSet(V expectedReference, V newReference,int expectedStamp, int newStamp),实现了只有当expectedReference与当前的Reference相等,且expectedStamp与当前引用值的stamp相等时才会发生设置,并且会将当前Reference的Stamp赋值为newStamp,否则set动作将会直接失败。

  1. AtomicMarkableReference

AtomicMarkableReference内部通过一个boolean值,实现Stamp,其CAS API和上面的ASR类似,因为boolean类型本身只有两种值,所以AMR实际上只能减少ABA问题的次数,无法做到完全规避ABA问题。

AtomicArray:

如果要实现原子性操作数组,可以使用AtomicIntegerArray等API,JUC提供了Integer,Long,Reference三种类型的AtomicArray,需要注意的是原子数组并不是说可以让线程以原子方式一次性地操作数组中所有元素的数组。而是指对于数组中的每个元素,可以以原子方式进行操作。

其核心实现的功能是,对于某个index位置元素的修改,通过CAS操作实现修改。对基于Unsafe实现的老版AtomicArray而言,需要做的就是通过index,和存储元素的size,以及Array的起始地址,就能计算出所要修改的元素的实际内存地址,然后对这个地址执行和AtomicInteger类似的CAS修改。

AtomicFieldUpdater:

要想使得共享数据的操作具备原子性,目前有两种方案。

第一,使用加锁配合volatile的方式进行(synchronize或者Lock),直接线程互斥操作,保证数据修改的原子性。

第二,将对应的共享数据定义成原子类型,比如将int定义成AtomicInteger,引用类型或者Atomic没有的数据类型可以借助于AtomicReference或者配合包装类的方式进行封装,借助底层CPU对lock和cmpxchg指令进行原子性修改。

但是有时候,我们不想或者不能通过上面的方式进行字段的原子性修改(比如一些依赖的三方库我们没办法去修改他的字段的情况下),这种场景,JUC提供了AtomicFieldUpdater,帮助我们完成对字段的原子性修改。

有三种Updater:AtomicFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。

使用方式以Netty中的源码为例:

class Example {
    // 声明Updater
    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater;
    static {
        // 初始化指明Updater作用在那个field上
        AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> updater = PlatformDependent.newAtomicIntegerFieldUpdater(AbstractReferenceCountedByteBuf.class,
                "refCnt");
        // ...
        refCntUpdater = updater;
    }
    // 必须用volative声明,保证多线程的可见性
    private volatile int refCnt = 1;
    @Override
    public ByteBuf retain() {
        // 自旋
        for (;;) {
            // 获取oldV
            int refCnt = this.refCnt;
            // ...
            //使用CAS对refcnt线程安全的操作
            if (refCntUpdater.compareAndSet(this, refCnt, refCnt + 1)) {
                break;
            }
        }
        return this;
    }
    // ...
}

但是!!!AtomicFieldUpdater实现CAS更新有很严格的限制:

  1. 未被volatile关键字修饰的字段无法被原子性地更新。
  2. 未被volatile关键字修饰的字段无法被原子性地更新。
  3. 无法直接访问的字段不支持原子性地更新,即字段必须是public类型的。
  4. final修饰的成员属性无法被原子性地更新。
  5. 父类的成员属性无法被原子性地更新。