原子类源码分析

AtomicInteger

  Java从1.5开始提供非阻塞的线程安全的包装类,例如AtomicInteger、AtomicLong等,这些实现大同小异,这里以AtomicInteger为例。

AtomicInteger的操作都基于Unsafe类,该类是JDK内部使用的一个工具类,提供硬件级别的原子操作,相关介绍戳https://www.jianshu.com/p/2e5b92d0962e。以下代码来自JDK1.8。
  首先来看一下成员变量:

 1 private static final Unsafe unsafe = Unsafe.getUnsafe();
 2 // value的内存偏移量
 3 private static final long valueOffset;
 4 
 5 static {
 6     try {
 7         // 获取value的内存偏移量
 8         valueOffset = unsafe.objectFieldOffset
 9             (AtomicInteger.class.getDeclaredField("value"));
10     } catch (Exception ex) { throw new Error(ex); }
11 }
12 //
13 private volatile int value;

   value是具体的值,该值是一个volatile变量,保证了内存可见性,另一个valueOffset变量是一个重要程度与value相当的一个变量,只要弄懂了这两个变量的作用,AtomicInteger就基本弄懂了,具体为什么需要这个变量,请看相关方法介绍。

  AtomicInteger类中的方法全部基于Unsafe,相关方法的作用请查看开头给出的链接或者自行google,这里只介绍一些方法来解释value与valueOffset的作用。

1 public final int get() {
2     return value;
3 }
4 
5 public final void set(int newValue) {
6     value = newValue;
7 }

  除了get与set方法之外,所有的方法全部使用valueOffset,至于原因,我的理解是因为volatile会禁止JVM进行内存重排序等相关优化。所以接下来看一下其他方法:

1 public final int getAndSet(int newValue) {
2     return unsafe.getAndSetInt(this, valueOffset, newValue);
3 }

  该方法是获得当前值然后将值设置为newValue,看一下Unsafe的方法:

1 public final int getAndSetInt(Object var1, long var2, int var4) {
2     int var5;
3     do {
4         var5 = this.getIntVolatile(var1, var2);// 原子获取地址为var1的地址加上偏移量var2的变量的值
5     } while(!this.compareAndSwapInt(var1, var2, var5, var4));// 自旋CAS操作,成功将值设置为var4则返回true
6 
7     return var5;// 返回旧值
8 }

  由上可以看出来该方法采用的是乐观锁机制,事实上Unsafe都是采用的乐观锁。
  set方法和lazySet方法的区别是原子包装类提及最多的问题,set采用volatile变量赋值保证了可见性但是会损失一定的性能(volatile写由于要加store-load屏障以及禁止重排序),lazySet采用的是Unsafe类的putOrderedInt方法,JDK官方对它的解释是,putOrderedInt方法之后会存在指令重排序,所以其可见性不能够保证,但是putOrderedInt使用的内存屏障(store-store)比volatile使用的内存屏障(store-load)性能更好,所以lazySet方式是保证了性能但是会损失可见性。
  总结一下,AtomicInteger是基于Unsafe类实现的,其中value存储的具体的值,主要作用是用于保证可见性而使用的,而valueOffset存储的是value的内存偏移量,在使用Unsafe类的方法时都是使用的该变量,主要作用是用于保证性能。

AtomicIntegerArray

  AtomicIntegerArray的方法同样是基于Unsafe类,跟AtomicInteger的方法类似,这里不做介绍,仅对成员变量做分析。我们已经知道Unsafe类的操作是基于内存偏移量,所以重点了解AtomicIntegerArray如何计算数组元素的内存偏移量:

 1 private static final Unsafe unsafe = Unsafe.getUnsafe();
 2 // 数据的首地址
 3 private static final int base = unsafe.arrayBaseOffset(int[].class);
 4 // 数组中元素的单位偏移量
 5 private static final int shift;
 6 // 存储数组
 7 private final int[] array;
 8 
 9 static {
10     // 获取数组中一个元素所占的字节数,由于是int所以占4个字节
11     int scale = unsafe.arrayIndexScale(int[].class);
12     if ((scale & (scale - 1)) != 0)
13         throw new Error("data type scale not a power of two");
14     // 2.获取单位偏移量
15     shift = 31 - Integer.numberOfLeadingZeros(scale);
16 }
17 
18 private long checkedByteOffset(int i) {
19     if (i < 0 || i >= array.length)
20         throw new IndexOutOfBoundsException("index " + i);
21 
22     return byteOffset(i);
23 }
24 
25 // 1.计算元素的真实偏移量
26 private static long byteOffset(int i) {
27     return ((long) i << shift) + base;
28 }

  在解释代码之前,先对了解一下数组地址分配。以int数组为例,int占4个字节,假设数组首地址是0,那么下标i=0的地址是0,i=1的地址是4,i=2的地址是8...,可以看出来,下标i跟地址的关系是addr=4*i=i<<2,如果数组首地址是base,则addr=(i<<2)+base。
  我们按照代码中标号的顺序解释:
  1. byteOffset方法是获取数组元素真实偏移量的,数组下标i左移shift位然后加上数组首地址就是数组首地址,这个shift我把它姑且叫做单位偏移量。
  2. numberOfLeadingZeros这个方法是获取参数的高位连续0的个数。scale的值是4,写成二进制就是0000 0000 0000 0000 0000 0000 0000 0100,最后numberOfLeadingZeros的返回值是29,因为1前面有29个0,所以最后计算出来shift=2。
  经过上面的解释大家应该已经知道了AtomicIntegerArray是如何计算数组中元素的地址的了。这里总结一下,我们把前面的addr=4*i+base做一个变形,假设数组元素占2的shift次幂个字节那么addr=[(2^shift)*i]+base=(i<<shift)+base,这就是一个所有基本类型通用的公式,也就是代码中byteOffset方法。

 

posted @ 2019-07-25 10:38  随花四散  阅读(373)  评论(0编辑  收藏  举报