Java原子类之AtomicIntegerArray源码分析

一、简介

AtomicIntegerArray可以用原子方式操作其元素的int数组,是对Integer数组支持的原子性操作。原子数组类与对应的普通原子类相比,只是多了通过索引找到内存中元素地址的操作而已。

注意:原子数组并不是说可以让线程以原子方式一次性地操作数组中所有元素的数组,而是指对于数组中的每个元素,可以以原子方式进行操作。

说得简单点,原子数组类型其实可以看成原子类型组成的数组

比如:

AtomicIntegerArray array = new AtomicIntegerArray(10);
array.getAndIncrement(0);   // 将第0个元素原子地增加1

等同于

AtomicInteger[] array = new AtomicInteger[10];
array[0].getAndIncrement();  // 将第0个元素原子地增加1

二、案例

class AtomicIntArrayDemo {

    public static void main(String[] args) {
        AtomicIntegerArray array = new AtomicIntegerArray(new int[]{45, 23, 13, 47, 12, 42});
        for (int i = 0; i < array.length(); i++) {
            final int j = i;
            new Thread(() -> {
                array.compareAndSet(j, 13, 31);
                array.getAndAdd(j, 2);
                array.decrementAndGet(j);
                array.getAndSet(j, array.get(j) - 1);
            }).start();
        }
        System.out.println(array);
    }
}

三、源码分析

3.1 构造方法和主要属性

AtomicIntegerArray其实和其它原子类区别并不大,只不过构造的时候传入的是一个int[]数组,然后底层通过Unsafe类操作数组:

public class AtomicIntegerArray implements java.io.Serializable {
    private static final long serialVersionUID = 2862133569453604235L;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 获取数组起始位置的偏移量
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    private static final int shift;
    private final int[] array;

    static {
        // 获取数据元素的大小(size),int类型的是 4
        int scale = unsafe.arrayIndexScale(int[].class);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
			
        // numberOfLeadingZeros:返回无符号整型i的最高非零位前面的0的个数,int类型(4)的前置0个数为29
        // 下面表达式返回位移量,用于计算下标。用于<<操作符,表示乘以2^shift  2^2=4	
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

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

        return byteOffset(i);
    }

    //计算第i个元素的偏移量
    private static long byteOffset(int i) {
        return ((long) i << shift) + base;
    }

    public AtomicIntegerArray(int length) {
        array = new int[length];
    }

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

可以看到,AtomicIntegerArray提供了两种构造器,本质都是内部利用array变量保存一个int[]数组引用。另外,AtomicIntegerArray利用Unsafe类直接操作int[]对象的内存地址,以达到操作数组元素的目的,几个关键的变量解释如下:

int base = unsafe.arrayBaseOffset(int[].class);

Unsafe类的arraBaseOffset方法:返回指定类型数组的第一个元素地址相对于数组起始地址的偏移值。

int scale = unsafe.arrayIndexScale(int[].class);

Unsafe类的arrayIndexScale方法:返回指定类型数组的元素所占用的字节数。比如int[]数组中的每个int元素占用4个字节,就返回4

那么,通过base + i * sacle其实就可以知道索引i的元素在数组中的内存起始地址。但是,观察AtomicIntegerArray的byteOffset方法,是通过i << shift + base的公式计算元素的起始地址的:

\[i << shift + base = i * 2^{shift} + base \]

这里,

\[2^{shift} \]

其实就等于scale

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

读者可以自己计算下:
shift = 31 - Integer.numberOfLeadingZeros(4) = 31 - 29 =2

之所以要这么绕一圈,其实是处于性能的考虑,通过移位计算乘法的效率往往更高。

3.2 获取指定下标元素 - get方法

get()方法主要利用byteOffset根据公式offset = base + i * scale求出数组对象的起始位置下标元素的偏移量offset,然后利用unsafe.getIntVolatile根据公式indexAddr = arrayAddr + offset求出出指定偏移位置的元素值

public final int get(int i) {
    return getRaw(checkedByteOffset(i));
}

// 下标检查,并计算数组对象起始位置至此下标元素的偏移量
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; // i*2^shift +base即,"i*元素间距+对象头长度"
}

private int getRaw(long offset) {
    // 利用unsafe方法求出指定偏移位置的int值
    return unsafe.getIntVolatile(array, offset);
}

3.2 更新指定下标元素 - set方法

set方法与get方法类似,都要先调用checkedByteOffset计算偏移量offset,不过此set方法要调用putIntVolatile在指定内存位置修改值而已。

public final void set(int i, int newValue) {
    unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
}

3.3 getAndSet方法

先获取值再设置新值,同样先调用checkedByteOffset计算偏移量offset,然后getAndSetInt进行CAS自旋更新指定的元素。

public final int getAndSet(int i, int newValue) {
    return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
}

// unsafe的方法
public final int getAndSetInt(Object o, long offset, int newValue) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, newValue));
    return v;
}

3.4 compareAndSet方法

CAS更新期望的元素,先调用checkedByteOffset计算偏移量offset,然后调用compareAndSwapInt尝试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);
}

四、AtomicIntegerArray的兄弟类

AtomicIntegerArray外,JDK提供了AtomicLongArray(长整型类型数组)、AtomicReferenceArray(引用类型数组)两种类型的原子数组。AtomicLongArrayAtomicReferenceArray的使用和源码与AtomicIntegerArray大同小异,具体请参考Oracle官方文档和源码。

AtomicIntegerArray对应AtomicIntegerAtomicLongArray对应AtomicLongAtomicReferenceArray对应AtomicReference

参考文章

posted @ 2022-06-30 16:32  夏尔_717  阅读(181)  评论(0编辑  收藏  举报