likes
comments
collection
share

Atomic原子类

作者站长头像
站长
· 阅读数 4

1、概述

     Atomic原子类位于java.util.concurrent.atomic包下面,支持单个变量上解除锁的线程安全编程,此包中的类可以将vlatile值、字段和数组的元素概念扩展到那些以提供原子条件更换操作的类中。

     对于原子操作,确切说应该是计算机硬件层面提供的原语,它是由一系列计算机指令组合而成的程序,具有不可分割特性,换句话说一旦开始就会一直运行到结束,中间不会被线程调度机制打断。

    Atomic的summary package.html

2、详情

    可以将包中的类分为五类:

  • 基本类型:AtomicBoolean、AtomicInteger、AtomicLong
  • 引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference
  • 数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 对象的属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • JDK1.8新增:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder

     AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的原子方式访问和更新功能。例如 AtomicBoolean 提供对int类型单个变量的原子方式访问和更新功能。每个类也为该类型提供适当的实用工具方法。例如,类 AtomicLong 和 AtomicInteger 提供了原子增量方法,可以用于生成序列号。

     AtomicStampedRerence 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。

     AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操作,对这些类型的数组提供了支持。例如 AtomicIntegerArray 是可以用原子方式更新其元素的int数组。

     AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater和AtomicLongFieldUpdater是基于反射的实用工具,可以提供对关联字段类型的访问。例如AtomicIntegerFieldUpdater可以对指定类的指定volatile int字段进行原子更新。

     DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder是JDK1.8新增的部分,是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

2.1、基本数据类型原子类

上面讲到基本数据类型的原子类有3种,AtomicInteger、AtomicBoolean、AtomicLong,以下以AtomicInteger为例讲解下,因为三者内部都是使用了CAS原语,以下为三者的区别:
1、AtomicBoolean内部将1作为true,0作为false,所以内部实现还是跟AtomicInteger一样;
2、AtomicLong是对长整形进行的原子操作,在32位操作系统中,64位的long 和 double变量由于会被JVM当作两个
分离的32位来进行操作,所以不具有原子性。而使用AtomicLong能让long的操作保持原子型。
3、jdk1.8之后出现了LongAdder可代替AtomicLong

     为什么只有这三种类型? 因为够用了,下面是摘抄子Atomic的summary        Additionally, classes are provided only for those types that are commonly useful in intended applications. For example, there is no atomic class for representing byte. In those infrequent cases where you would like to do so, you can use an AtomicInteger to hold byte values, and cast appropriately. You can also hold floats using Float.floatToRawIntBits(float) and Float.intBitsToFloat(int) conversions, and doubles using Double.doubleToRawLongBits(double) and Double.longBitsToDouble(long) conversions.

AtomicInteger内部包含重要的3个属性(除了序列化的serialVersionUID),分别是Unsafe的引用,在内存中对象的偏移量valueOffset,以及指向正真值的value,这里的value使用了volatile修饰,目的是为了值在内存和高速缓存的可见性。

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    private volatile int value;

AtomicInteger的构造函数也十分简单,除了默认的构造函数之外,还有一个传入初始值的构造器。

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    }

下面列举AtomicInteger常用的方法(个人认为:对于原子类值就是它们的状态)

    //传递希望当前内存中状态应该是expect值,然后修改为update
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
    //这里和compareAndSet一样,据说在jdk8之后又变化
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
    //先返回旧值,然后再+1
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    //和getAndIncrement相反,先返回旧值,再-1
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
    
    /**
     *这里是返回+1后的值,所以先执行Unsafe的getAndAddInt加1,然后返回旧值的同时再+1
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    
    ...省略其他方法(其中还包括3个1.8之后新增的IntUnaryOperator函数式编程接口)

从上面列举出来的方法可以看到,AtomicInteger内部实际调用的式Unsafe.compareAndSwapInt, 注意方法名虽然是compareAndSet,但实际内部在Unsafe调用的是compareAndSwapInt, 由于内部是Native方法,就没再确定究竟是使用了CompareAndSwap还是CompareAndSet哪个原语,看返回值应该是CompareAndSet,但是看方法名确实CompareAndSwap,这两个原语的区别在于返回值不同。

但是很明显,在AtomicInteger内部应该是使用了CAS操作来保证原子性。在其中一个方法getAndIncrement中,进入内部可以看到也是使用了CAS操作

    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;
    }

     从上述代码中我们可以得出,会先获取内存中存储的值,最终会调用compareAndSetInt()方法来完成最终的原子操作。其中compareAndSetInt()方法的返回值代表着该次CAS操作是否成功。如果不成功。那么会一直循环。直到成功为止(也就是循环CAS操作)。

    在一个死循环内不断的尝试修改目标的值,直到修改成功。如果在竞争不激烈的情况下,它修改成功的概率很高,否则的话修改失败的概率就会很高, 在大量修改失败的时候这些原子操作就会多次循环尝试, 因此性能就会受到影响。

     这里简要的对CAS操作进行描述:CAS操作内部实现原理是缓存锁,在其操作期间,会修改对应操作对象的内存地址。同时其会保证各个处理器的缓存是一致的,如果处理器发现自己的数据对应的内存地址被修改,就会将当前缓存的数据处理为无效,同时该处理器会重新从系统内存中把数据处理到缓存中。这就是解决缓存一致性的MESI。

2.2、数组类型原子类

     对于数组类型的原子类,在Java中,主要通过原子的方式更新数组里面的某个元素,数组类型原子类主要有以下几种:

  • AtomicIntegerArray:Int数组类型原子类
  • AtomicLongArray:long数组类型原子类
  • AtomicReferenceArray:引用类型原子类

下面以AtomicIntegerArray为例

public static void main(String[] args)  {
        int[] array = new int[]{1,2,3};
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);
        atomicIntegerArray.compareAndSet(1,2,4);
        System.out.println(Arrays.toString(array));
}

输出结果如下:

[1, 2, 3]

为什么使用原子数组后无法修改呢,原因在于AtomicIntegerArray的构造函数上,这里我们传递了一个数组的引用进去

    public AtomicIntegerArray(int[] array) {
        // Visibility guaranteed by final field guarantees
        this.array = array.clone();
    }

从这里我们可以看到,传递一个数组引用进去之后,会调用数组的clone方法创造一个新的同长度同元素的数组。对于一维数组而言clone是深克隆(二维数组是浅克隆),所以说这里的AtomicIntegerArray不是直接修改原来的数组,直接数组AtomicIntegerArray

    public static void main(String[] args)  {
        int[] array = new int[]{1,2,3};
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);
        atomicIntegerArray.compareAndSet(1,2,4);
        System.out.println(atomicIntegerArray);
    }

输出结果如下:

[1, 4, 3]

AtomicIntegerArray这里包含了4个属性

    //Unsafe的引用,负责获取和计算数组中各个元素的地址
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //数组中首个元素的距离数组的偏移量,因为java的数组可能前面含有对象头等等属性
    //所以数组首地址不等于首元素的地址
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    //用来计算数组元素的大小
    private static final int shift;
    //clone后的数组
    private final int[] array;

AtomicIntegerArray内部也是使用了CAS的操作

    public final boolean compareAndSet(int i, int expect, int update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }

    private boolean compareAndSetRaw(long offset, int expect, int update) {
        return unsafe.compareAndSwapInt(array, offset, expect, update);
    }

使用CAS的时候都是需要传递数值在内存中的地址,这是AtomicIntegerArray就需要计算元素在内存中的地址。 由上面的方法可以看到执行compareAndSet的时候,会传递数组的下标给方法checkedByteOffset去计算内存中的 地址。而checkedByteOffset反过来又会调用byteOffset这个方法

    private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);

        return byteOffset(i);
    }

    private static long byteOffset(int i) {
        return ((long) i << shift) + base;
    }

上面提到了一个AtomicIntegerArray的一个属性base,这个通过调用Unsafe.arrayBaseOffset()返回了数组第一个元素地址举起数组的起始地址的偏移量,之后通过在静态域中调用数Unsafe.arrayIndexScale方法返回每个元素的占内存的字节大小,通过base+i*scale就可以计算出来了每个元素在内存中的偏移量(i表示数组的下标)。

看AtomicIntegerArray的static域如何初始化各个属性

    static {
        int scale = unsafe.arrayIndexScale(int[].class);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

然而Atomic的作者更巧妙通过移位的方式计算偏移量,byteOffset这个方法是通过公式计算的

                            i << shift + base = i * 2^shift + base

也就是说 scale = 2^shift

shift = 31 - Integer.numberOfLeadingZeros(scale),Integer.numberOfLeadingZeros(scale)是将scale转换为2进制,然后从左往右数连续0的个数。