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方法。