引言

这篇文章,我们来看JDK原子类中最经典的AtomicInteger类。

类的层次结构

  1. public class AtomicInteger extends Number implements java.io.Serializable {}

它的继承层次很简单,就是继承了Number类,Number类是很多简单类型包装类的父类,我已经在这篇文章简单介绍过这个类,这里不再赘述。

value、valueOffset字段与内存位置定位

AtomicInteger有两个简单类型的字段,分别是:

  1. private static final long valueOffset;
  2. private volatile int value;

value就是这个原子包装类的int值,我们之后会经常来分析它。这里我们先重点看下valueOffset。首先,它是一个静态常量,意味着每个AtomicInteger都共享这个值,那么这个值是什么呢?我们来看它的赋值,它是在静态块内进行赋值的:

  1. static {
  2. try {
  3. valueOffset = unsafe.objectFieldOffset
  4. (AtomicInteger.class.getDeclaredField("value"));
  5. } catch (Exception ex) { throw new Error(ex); }
  6. }

调用的是unsafe.objectFieldOffset这个方法,传入的是AtomicInteger的value这个Field。
再看unsafe.objectFieldOffset这个方法:

  1. public native long objectFieldOffset(Field var1);

是一个native方法。从名字上看,好像获取的是参数指定的字段相对于参数所在对象的内存偏移量,如果你了解堆上对象的内存布局,就会知道,一个对象在内存中存储布局分为三个区域:对象头、实例数据和对齐填充,在AtomicInteger对象中,value作为实例字段是存储在实例数据部分的。AtomicInteger静态块中的代码就是通过unsafe的objectFieldOffset方法获取了value这个字段值相对于AtomicInteger这个对象开始位置的内存偏移量。
我们可以来验证一下:

  1. public class AtomicIntegerValueOffsetTest {
  2. public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
  3. AtomicInteger atomicInteger = new AtomicInteger();
  4. Field valueOffset = atomicInteger.getClass().getDeclaredField("valueOffset");
  5. valueOffset.setAccessible(true);
  6. Long valueOffsetValue = (Long) valueOffset.get(atomicInteger);
  7. System.out.println("valueOffset的值是"+valueOffsetValue);
  8. System.out.println(ClassLayout.parseInstance(atomicInteger).toPrintable());
  9. }
  10. }

我们首先用反射获取了valueOffset的值,然后用ClassLayout输出了AtomicInteger这个对象的内存布局。ClassLayout是org.openjdk.jol包里面的类,用来解析对象的内存布局,关于ClassLayout的使用,可以参考这篇文章。看输出结果:

  1. valueOffset的值是12
  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. java.util.concurrent.atomic.AtomicInteger object internals:
  4. OFFSET SIZE TYPE DESCRIPTION VALUE
  5. 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
  6. 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  7. 8 4 (object header) bb 3d 00 f8 (10111011 00111101 00000000 11111000) (-134201925)
  8. 12 4 int AtomicInteger.value 0
  9. Instance size: 16 bytes
  10. Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看到valueOffset的值是12,而输出的内存布局中,value也是从第12个字节开始的,这就证明了我们的猜想。
那AtomicInteger获取value字段的内存偏移量用来做什么呢?应该是用来在内存中定位value这个字段的,如果你还记得CMPXCHG这个指令,它的目的操作数是一个内存位置,我们需要提供这个内存位置,有了当前AtomicInteger对象(this)和偏移量,就能直接得到这个内存位置了。valueOffset这个字段在AtomicInteger中的很多方法中都会用到,下面,我们来看AtomicInteger几个重要的方法。

重要方法

compareAndSet()方法

  1. public final boolean compareAndSet(int expect, int update) {
  2. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  3. }

compareAndSet方法在value的当前值等于期待值(expect参数的值)时,原子地将AtomicInteger的值设置为给定值(update参数的值)。
这个正是CMPXCHG这个指令的语义。它是通过unsafe.compareAndSwapInt这个方法实现的,这是一个native方法:

  1. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

之后我们会看到,原子包装类的很多原子操作都是调用了这个方法。我们需要关注它的参数,一个是当前AtomicInteger对象(this),一个是valueOffset(value值相对于AtomicInteger的偏移量),预期值和更新值。AtomicInteger和valueOffset两个参数应该是用来确定value的内存位置的,也就是这两个参数一起,实现的是CMPXCHG指令中的内存位置这个操作数的含义,预期值和更新值就分别对应于CMOXCHG指令中的旧的预期值和新值。Unsafe类中的compareAndSwapInt()这个方法实际上就是CMPXCHG指令。
还有一点需要注意的是,comparAndSet方法只会调用一次LOCK CMPXCHG,不管是否成功,都会返回,返回值会告诉调用者更新是否成功。

getAndIncrement()方法

  1. public final int getAndIncrement() {
  2. return unsafe.getAndAddInt(this, valueOffset, 1);
  3. }

这个方法我们应该经常会遇到,它保证了对value的原子自增操作。它调用的是unsafe的getAndAddInt方法:

  1. public final int getAndAddInt(Object var1, long var2, int var4) {
  2. int var5;
  3. do {
  4. var5 = this.getIntVolatile(var1, var2);
  5. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  6. return var5;
  7. }

这个方法在一个持续的循环中,每次都是通过getIntVolatile方法取到value的当前值,然后使用compareAndSwapInt方法或者说LOCK CMPXCHG指令原子性地将这个当前值与比它大1的值交换,如果操作成功,就结束循环,如果没有,说明value已经被修改,就继续循环直到更新成功。这与上面说的compareAndSet()方法不同,compareAndSet()方法只会执行一次,不管是否更新成功。
更新不成功继续循环就是忙等待(忙循环)了,线程并不会被阻塞,而是一直在cpu上面执行。我们知道LOCK CMPXCHG只是一条指令而已,它执行完就是结束了,循环是软件(这里就是compareAndSwapInt这个方法)自己来实现的。
getIntVolatile方法通过当前AtomicInteger对象和valueOffset来获取value的当前值。为什么AtomicInteger中的value是volatile,这个我们会在将java内存模型时讲到,这里先不要重点关注它。

getAndAddInt()方法

这个方法与getAndIncrement方法类似,只是增加的不是1,而是一个参数指定的值。

  1. public final int getAndAdd(int delta) {
  2. return unsafe.getAndAddInt(this, valueOffset, delta);
  3. }

它也是调用的unsafe.getAndAddInt()方法,同样会一直循环直到更新成功,这里不再重复。

Unsafe类的原子方法

Unsafe类中的compareAndSwapInt、compareAndSwapLong和compareAndSwapObject这三个native方法就是LOCK CMPXCHG这个原子指令在jdk中的映射:

  1. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  2. public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
  3. public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

以compareAndSwapInt为例,它的前两个参数,用来进行内存位置的定位,也就是CMPXCHG命令中代表内存位置的的目的操作数,后面两个参数分别就是预期的旧值和要更新成的新值。
除了这三个方法,Unsafe还提供了getAndAddInt、getAndAddLong、getAndSetInt、getAndSetLong和getAndSetObject这五个方法来实现忙循环的原子更新,它们的实现方式都是在一个循环中去调用上面的三个方法,直到更新成功。以getAndAddInt为例,它的逻辑是这样的:

  1. public final int getAndAddInt(Object var1, long var2, int var4) {
  2. int var5;
  3. do {
  4. var5 = this.getIntVolatile(var1, var2);
  5. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  6. return var5;
  7. }

它调用的就是compareAndSwapInt这个方法。
我们在上面AtomicInteger的讲解中已经看到了这些方法的使用,后面我们要介绍的AtomicObject和AtomicIntegerArray也会调用这些方法,这里我们梳理清楚了这些方法的逻辑,对理解之后的原子类会有帮助。
现在,我们用AtomicInteger来修改之前一个不是线程安全的代码:

  1. public class UnsafeCount {
  2. private static AtomicInteger count = new AtomicInteger();
  3. public static void getNext(){
  4. count.incrementAndGet();
  5. }
  6. public static void main(String[] args) throws InterruptedException {
  7. Thread thread1 = new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. for(int i=0;i<500;i++){
  11. getNext();
  12. }
  13. }
  14. });
  15. Thread thread2 = new Thread(new Runnable() {
  16. @Override
  17. public void run() {
  18. for(int i=0;i<500;i++){
  19. getNext();
  20. }
  21. }
  22. });
  23. Thread thread3 = new Thread(new Runnable() {
  24. @Override
  25. public void run() {
  26. for(int i=0;i<500;i++){
  27. getNext();
  28. }
  29. }
  30. });
  31. thread1.start();
  32. thread2.start();
  33. thread3.start();
  34. //等待较长时间 让执行自增操作的线程完成
  35. Thread.sleep(5000);
  36. System.out.println(count);
  37. }
  38. }

在之前的版本中,count是一个int类型,我们改成了AtomicInteger,再次执行的结果总是1500。

小结

如果你理解了LOCK CMPXCHG命令,那么AtomicInteger类或者Unsafe类中的方法理解应该就很容易。LOCK CMPXCHG指令本质上就是原子性地根据预期值替换内存位置的值的操作,它并不是为自增设计的,但是AtomicInteger和Unsafe巧妙地利用这个功能,将它的更新值参数设置为value的当前值加1或者加某一个数,这样来实现自增1或者自增n的原子操作。