引言

AtomicIntegerArray表示一个元素可以被原子更新的数组。

类声明

AtomicIntegerArray的声明如下:

  1. public class AtomicIntegerArray implements java.io.Serializable {}

shift和base字段

AtomicIntegerArray类中有两个字段base和shift,这两个字段用来定位要进行更新的元素的内存位置。回想AtomicInteger类中的valueOffset这个字段,它是AtomicInteger对象中value字段相对于这个对象的内存偏移量,根据AtomicInteger对象本身(this)和这个偏移量,我们就能定位这个内存位置进而提供给LOCK CMPXCHG命令使用。在AtomicIntegerArray中,我们同样需要解决内存位置的问题,但是在数组中,要定位一个元素(元素i)的位置,就与AtomicInteger不一样了,首先,我们需要知道在这个数组的对象布局中,实例数据是从哪里(哪个偏移位置)开始的,然后需要知道元素i在整个实例数据部分的偏移量,这两个偏移量加起来,才能定位元素i,base和shift这两个字段就是用来找到这两个偏移量的。
先看base字段的赋值:

  1. private static final int base = unsafe.arrayBaseOffset(int[].class);

调用的是Unsafe字段的arrayBaseOffset方法,这个方法就是用来确定在int数组中,实例数据部分相对于整个数组对象的偏移量的。我们可以用下面的方法验证一下:

  1. public class AtomicIntegerValueOffsetTest {
  2. public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
  3. AtomicIntegerArray aia = new AtomicIntegerArray(10);
  4. Field baseField = aia.getClass().getDeclaredField("base");
  5. baseField.setAccessible(true);
  6. int base = (int) baseField.get(aia);
  7. System.out.println(base);
  8. System.out.println(ClassLayout.parseClass(int[].class).toPrintable());
  9. }
  10. }

我们先用反射获取了base的值,然后用ClassLayout输出了int数组的内存布局,看输出:

  1. 16
  2. # WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
  3. [I object internals:
  4. OFFSET SIZE TYPE DESCRIPTION VALUE
  5. 0 16 (object header) N/A
  6. 16 0 int [I.<elements> N/A
  7. Instance size: 16 bytes
  8. Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

base的值是16,输出的内存布局中,对象头占用16个字节,实例数据从第16个字节开始,正好与base的值是对应的,证明了我们的猜想是正确的。
所以base就表示的是int数组中实例数据部分相对于整个数组对象的偏移量。我们还需要一个元素i相对于实例数据部分的偏移量,这个其实很简单,因为每个int在java中占用4个字节,这个偏移量直接用i乘以4就能得到,AtomicIntegerArray中不是这样做的,它用的是位移的方式,看shift字段的赋值,它的赋值是在静态块中进行的:

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

scale就是int数组中每个元素占用的字节数,也就是4,然后shift是31减去Integer.numberOfLeadingZeros(scale)的值,Integer.numberOfLeadingZeros这个方法计算的是参数scale用二进制形式(总共32位)表示后第一个非零值前面的连续0的个数,4的二进制表示中这个值是29,所以shift就是2,shift的意思是,对于数组中的元素i,我们不用4*i,而是将i左移shift位,得到的就是第i个元素相对于元素实例数据部分的偏移量。这个在checkedByteOffset和byteOffset方法中能体现出来:

  1. private long checkedByteOffset(int i) {
  2. if (i < 0 || i >= array.length)
  3. throw new IndexOutOfBoundsException("index " + i);
  4. return byteOffset(i);
  5. }
  6. private static long byteOffset(int i) {
  7. return ((long) i << shift) + base;
  8. }

理解了AtomicIntegerArray中元素数组的内存定位,我们来看几个重要的方法:

getAndSet()方法

getAndSet方法用来原子性的将元素i更新为新值newValue。

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

这个方法有两个参数,i表示要更新的元素的索引也就是我要更新第几个元素,newValue表示要更新成的新值。它调用的是unsafe的getAndSetInt方法,这个方法我们在AtomicInteger中已经介绍过了,它循环调用比较compareAndSwapINt方法来进行比较并替换操作直到更新成功。我们看AtomicIntegerArray提供给它的三个参数:array表示当前int数组的内存地址,checkedByteOffset(i)表示元素i相对于这个数组的总偏移量,这两个参数一起来定位要更新的元素的内存位置,newValue就是新值,这样,CAS命令就能得到它的三个参数:内存位置,旧值(根据内存位置可以拿到)和新值。

getAndAdd方法

getAndAdd方法用来原子性的将元素i的值增加delta。

  1. public final int getAndAdd(int i, int delta) {
  2. return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
  3. }

它调用的是Unsafe的getAndAddInt方法,这个方法我们在AtomicInteger中也已经介绍过,这里不再赘述。
AtomicIntegerArray的getAndIncrement(自增)、getAndDecrement(自减)都是调用getAndAdd方法实现的。