上一篇文章我们已经大致了解了AtomicInteger的实现机制以及在jdk 1.8上的新特点,现在我们趁热打铁来看看AtomicIntegerArray类。
同样的,我们先看看AtomicIntegerArray的简单用法:
1 | int[] intArray = new int[]{1, 2, 3, 4, 5}; |
用法同样非常简单,操作的数据类型类似int[], 只不过封装好套了一层atomin操作。经过上一篇AtomicInteger的介绍,上面的各个基本的方法使用的结果相信应该不会有什么问题,
并且实际上都是可以根据方法名“顾名思义”的。
同样的,我们看看源码, 基于jdk 1.8.0_05 。
1 | private final int[] array; |
这里的构造方法也很简单,一种是传入数组的长度,默认创建一个length长度的各个元素为0的数组,一种是直接在外面创建好一个数组,然后传给构造方法。
这里保存数组的值是全局维护了一个int[] array. 有没有发现这里和AtomicInteger的不同?AtomicInteger保存值是维护了一个volatile来保证可见性,这里为什么没有采取同样的方法?
仔细看看一下,array使用的是final修饰,变成了常量数组,引用不可变,这个array数组就保存到了方法区,同样的可以保证多线程访问时的可见性,避免使用volatile也减少了开销。
类似AtomicInteger,AtomicIntegerArray也是采用了Unsafe特殊类来提供CAS函数进行原子性的操作,这块暂且按下不表,我们看看AtomicIntegerArray的内部实现中相比AtomicInteger多了一些有意思的存在,
一个是base, 一个是shift。
1 | private static final Unsafe unsafe = Unsafe.getUnsafe(); |
1 | Unsafe.class |
arrayBaseOffset方法一般是配合arrayIndexScale方法使用,两个都是属于Unsafe类中的native方法。这两个native方法需要传入的参数都是一个array类型的class。arrayBaseOffset是能获取数组首个元素的首地址偏移,arrayIndexScale可以用来获取数组元素的增量地址的方法。上面那段代码和内容可能有点不太好理解,我们先形成这个印象,接下来可以先看一个小例子。
1 | int base = unsafe.arrayBaseOffset(int[].class); |
我们用来计算offsetForIdx的过程,就是先计算基址加上一个偏移的增量。其中偏移的增量又是根据偏移的索引和元素增量地址的乘积获得。
如果还不清楚,我们更深入一点来研究这两个native方法: -)。
1 | public int arrayBaseOffset(Class clazz) { |
上面一段是我抠的一段源码,可以看到,arrayBaseOffset实质上做的是获取一个数组对象在内存中从数组的地址到首个元素的地址的偏移量。为什么offset会先加12呢,这里涉及到的是java内存中对象存储的知识。
我们要知道,每个类对象在内存中存储时除了数据内容,其实还要包含一个头部信息的,主要是8字节大小的元信息,存储一些标识符号等信息,如果这个类是数组类型的话,还需要4字节来存储数组的大小,所以
这里是12字节。接下来又涉及到了字节对齐,我们知道在jvm中,是要以8字节为单位进行对齐的,这里的头部12字节肯定是无法对齐了,但是如果是long,double等8字节的类型,就是在开始存时就进行对齐操作,
这样就能保证接下来的每一个元素都是8的倍数,而如果是其他的对象比如int 4字节,就在数组末尾进行对齐,这样就能缺多少补多少。
而arrayIndexScale实际上是能获取数组中每个元素在内存中的大小,是不是有点像cpp里sizeof的感觉了?
分析到这里我们应该对AtomicIntegerArray的base和scale有了一个清晰的认识了,但是我们会发现,在最初我们贴出的那段static代码块中,将scale转为了一个final int值shift。
首先通过scale & (scale-1) !=0 进行一下检查,事实上n & (n-1) ==0 就说明n要么是0,要么是2的幂次方,这里的这个检查处理结合上面贴出的arrayIndexScale中对基本类型的处理就非常好理解了。
接着用到了Integer.numberOfLeadingZeros(scale)方法,这个方法能够获取scale中高位的0的个数,因为一个int是以32位二进制存储的,当高位没有时,都会补0。所以shift就是第一个不为0的index,这里非常重要,
也就是说,shift可以理解为scale的2幂次方的这个幂。例如scale为16,那么shift就是4, scale为8,shift就是3。好了,关于AtomicIntegerArray的初始化构建到这儿就有一个了解了。但是知道这个有什么用呢?我们接下来会慢慢用到。
我们再来看两个重要的基础方法。
1 | private long checkedByteOffset(int i) { |
checkedByteOffset接到的参数就是array中的index,检查一下没有数组下标越界后实际上做的事到了byteOffset。i << shift其实就是i * scale。这也符合我们开始时解释arrayBaseOffset时举的例子。
本质上还是计算了CAS中需要的那个内存中的旧值。所以这里的转化就非常巧妙,可以再多回味一下。
好了,基础工作我们基本分析完了,现在像学习AtomicInteger一样来学习一下存取方法吧。
1 | public final int getAndAdd(int i, int delta) { |
这两个核心方法的思想和实质几乎和AtomicInteger一模一样,除了修改值时需要传入一个数组的index,最后都是进了Unsafe类中去getAndAddInt,然后走compareAndSwapInt方法,到这里的过程就和AtomicInteger一模一样了。
同样的,在jdk 1.8中加入了单值运算操作。
1 | public final int getAndUpdate(int i, IntUnaryOperator updateFunction) { |
用法和AtomicInteger差不多,多传入一个index.
1 | int[] intArray = new int[]{1, 2, 3, 4, 5}; |
那么aia变成了[1, 0,3,4,5]。
除了直接更新值操作,也和AtomicInteger一样新增提供了getAndAccumulate和accumulateAndGet方法,都可以传入一个IntBinaryOperator进行java 8特性的编写。这也是非常方便的了。
ok,关于AtomicIntegerArray的探究就到这儿吧,AtomicLongArray等原子数组类型和这个就差不多了,只是数据类型对象更换一下,具体的机制是想通的,大家有兴趣可以自己再看看相关的源码。