阅读 58

Java juc笔记 [1] - atomic包

Atomic包简介及分类

java.util.concurrent(一般简称juc)包下的atomic包提供了一系列在并发场景下尽量无锁实现原子操作的类,其核心思想是使用CAS+循环实现轻量级乐观锁,在并发竞争不激烈的情况下效率会比加锁实现好很多。除Striped64JCP JSR-166 Expert Group成员协助完成外,作者都是Doug Lea大师。虽然juc包整体代码量不大但每次看都有新的思考和收获。

原子包装类:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
  • AtomicReference
  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
  • AtomicStampedReference
  • AtomicMarkableReference

属性更新类:

  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • AtomicReferenceFieldUpdater

高并发计算类:

  • Striped64
  • LongAdder
  • DoubleAdder
  • LongAccumulator
  • DoubleAccumulator

介绍

原子包装类

一些通用属性:

  • serialVersionUID 序列化版本号
  • unsafe 实现具体CAS操作的对象
  • valueOffset 用于CAS操作的配置,记录的是value的具体地址,可理解为具体CAS操作的地址
  • value 使用volatile修饰的实际值,对这个值的修改会对其他线程立即可见,这也就是atomic包存在的意义

一些通用方法:

  • get 获取当前值
  • set 不考虑原值直接设置当前值
  • compareAndSet CAS设置当前值
  • weakCompareAndSet CAS设置当前值,JAVA 8中实际与compareAndSet结果完全相同,一些参考资料表示某些实现中weakCompareAndSet在compare失败时立刻返回false
  • lazySet 延迟设置值,减少内存屏障,但设置的值并非立刻对其他线程可见,与atomic定义似乎相关性不大,一般用不到
  • getAndSet get+set

AtomicBoolean

提供对boolean的原子操作,内部使用int作为底层操作类型而不是boolean类型参考资料认为由于跨平台特性,JAVA的Unsafe机制的最小操作类型是int类型,即Unsafe类只提供了compareAndSwapObject/compareAndSwapInt/compareAndSwapLong几种CAS操作。

AtomicInteger

提供对int的原子操作,getAndIncrement等一系列自增/自减等方法依赖unsafe.getAndAddInt方法实现,即底层都是compareAndSwapInt循环操作,包括getAndUpdate/getAndAccumulate等方法也是如此。

AtomicLong

提供对long的原子操作,大体与AtomicInteger差不多,底层使用compareAndSwapLong循环操作。

同时,判断了当前VM是否支持对long类型直行CAS,如果不支持可能在CAS时进行额外的加锁操作。

AtomicReference

同上,提供对Object引用的原子操作,底层使用compareAndSwapObject循环操作。

AtomicIntegerArray

底层操作int[]类型,通过计算数组元素的偏移量对对应的元素值进行原子操作(也只能对元素进行操作),这要求底层数组在内存中需要是物理连续的。由于int[]对象内部元素并非volatile,即内部元素不是直接对其他线程具有可见性的,因此对于每个元素的操作是通过unsafe方法实现的如unsafe.getIntVolatile,具体方式未知,猜测是通过加锁或者虚拟机层面支持的原子操作。

AtomicLongArray / AtomicReferenceArray

思路与AtomicIntegerArray相同。

AtomicStampedReference / AtomicMarkableReference

仍然是原子包装类,不过包装的对象由原本的单个值改为一组值:

  • AtomicStampedReference<V> 包装了一个Pair<V>,可以理解为对二元组<V, int>处理,对整个pair统一CAS处理,可以解决CAS的ABA问题
  • AtomicMarkableReference 同上,二元组为<V, boolean>

AtomicFieldUpdater类

AtomicFieldUpdater将原子包装类的功能进行了拆分,将值的具体保存位置与原子操作分离。示例:

public class Test {

    // 保存位置
    private volatile int value;

    // 更新位置,最好使用static final修饰
    private static final AtomicIntegerFieldUpdater<Test> valueUpdater = AtomicIntegerFieldUpdater.newUpdater(Test.class, "value");

    public void printValue() {
        System.out.println(value);
    }

    public void incrementValue() {
        //value++;    // 显然不可以这么做
        valueUpdater.incrementAndGet(this);
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.printValue();
        test.incrementValue();
        test.printValue();
    }

}
复制代码

在分离后,value在并发取值等操作上可以如非包装类一样操作,但并发更新操作上应通过updater操作。

updater的原理:

  1. AtomicFieldUpdater类都是抽象类,CAS的具体方法(如compareAndSet)均为抽象方法,而内部类继承了这个抽象类并实现了其中的CAS相关方法
  2. AtomicFieldUpdater类提供的方法与原子包装类提供的方法一致
  3. 内部类通过反射获取真正需要操作的目标属性,构造方法签名以AtomicIntegerFieldUpdater为例:AtomicIntegerFieldUpdaterImpl(final Class<T> tclass, final String fieldName, final Class<?> caller)
  4. 因为是通过反射实现的原子操作,updater需要进行很多验证操作,包括属性的可见性、类型检查等,尤其在每个原子操作前都进行了accessCheck确保输入的调用方对象(上述代码的test)与构造方法传入的类(Test)一致

根据原理我们可以知道,使用AtomicFieldUpdater需要遵循一些约定:

  • 操作的属性需要使用volatile修饰,否则无法保证线程可见性
  • 操作的属性需要对于updater可见,否则无法访问
  • 操作的属性不能使用static修饰,因为updater通过反射操作的是实例的属性
  • 操作的属性不能使用final修饰,否则无法更新
  • 操作的属性与updater可操作的类型必须一致,如long类型对应AtomicLongFieldUpdater
  • updater可操作类的指定对象,最好使用static final修饰,否则不如直接使用原子包装类

实际上使用updater操作和直接使用原子包装类的作用是一样的,完全可以实现相同的功能,而由于updater使用反射获取目标对象的属性且每次操作需要做前置检查,使得updater不光限制条件多,性能还不如直接使用原子包装类。那么我们为什么要用updater呢?参考资料给出了两种合理的使用场景:

  1. 多数情况下希望按照非包装类使用(即volatile修饰,可以直接使用 + - * / 连接),偶尔需要通过原子操作更新时
  2. 由于一个static修饰的updater实例可以对调用方(上述Test类)的所有对象起作用,使用原子包装类会比非包装类占用更多的内存,当存在大量调用方对象时尤其如此,这时候使用updater可以极大减少内存占用

AtomicIntegerFieldUpdater / AtomicLongFieldUpdater / AtomicReferenceFieldUpdater

分别对应int/long/Object的属性更新。

其中AtomicLongFieldUpdater通过上述AtomicLong判断VM是否支持long类型CAS操作的结果,提供了两种updater:

        if (AtomicLong.VM_SUPPORTS_LONG_CAS)
            return new CASUpdater<U>(tclass, fieldName, caller);
        else
            return new LockedUpdater<U>(tclass, fieldName, caller);
复制代码

CASUpdater通过unsafe中的CAS方法进行原子操作,而LockedUpdater通过synchronizedupdater对象加锁实现原子更新。

AtomicReferenceFieldUpdater除了accessCheck对可见性检查外,还通过valueCheck对null值和值的类型进行检查。

高并发计算类

之前写的Java并发累加器一文介绍过,这里直接copy一部分过来。

Striped64

内部包含子类Cell,实际上就是原子包装类功能的子集,只保存value及其CAS功能,但没有其他诸如get、incrementAndGet等功能。并且Cell类使用@Contended注解避免伪共享问题。

内部定义了transient volatile long base,用于“保存值的一部分”;还定义了transient volatile Cell[] cells,用于“保存值的另一部分”。没错,base和cells共同组成了最终的值

在并发程度较高时,原子包装类使用的CAS操作失败频率也较高,且多了很多不必要的资源消耗,导致性能下降。

Striped64考虑到原子包装类中CAS竞争的资源单一(都在竞争value资源),选择在有冲突时分散竞争资源,为每个线程分配一个Cell,让每个资源竞争对应的资源(cell),大幅减少冲突,在特定的高并发场景性能显著优于原子包装类。详情参见旧文。

LongAdder / DoubleAdder

继承Striped64,功能上只是简单实现了累加器所需的方法,使用的是null二元计算,即使用默认的加法。详情参见旧文。

LongAccumulator / DoubleAccumulator

同上,不过使用的是自定义的二元计算方法。

参考资料

multithreading - Why java.util.concurrent.atomic.AtomicBoolean is internally implemented with int? - Stack Overflow

The Atomic classes in Java: atomic field updaters

本文搬自我的博客,欢迎参观!