引言
这篇文章,我们来看JDK原子类中最经典的AtomicInteger类。
类的层次结构
public class AtomicInteger extends Number implements java.io.Serializable {}
它的继承层次很简单,就是继承了Number类,Number类是很多简单类型包装类的父类,我已经在这篇文章简单介绍过这个类,这里不再赘述。
value、valueOffset字段与内存位置定位
AtomicInteger有两个简单类型的字段,分别是:
private static final long valueOffset;
private volatile int value;
value就是这个原子包装类的int值,我们之后会经常来分析它。这里我们先重点看下valueOffset。首先,它是一个静态常量,意味着每个AtomicInteger都共享这个值,那么这个值是什么呢?我们来看它的赋值,它是在静态块内进行赋值的:
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
调用的是unsafe.objectFieldOffset这个方法,传入的是AtomicInteger的value这个Field。
再看unsafe.objectFieldOffset这个方法:
public native long objectFieldOffset(Field var1);
是一个native方法。从名字上看,好像获取的是参数指定的字段相对于参数所在对象的内存偏移量,如果你了解堆上对象的内存布局,就会知道,一个对象在内存中存储布局分为三个区域:对象头、实例数据和对齐填充,在AtomicInteger对象中,value作为实例字段是存储在实例数据部分的。AtomicInteger静态块中的代码就是通过unsafe的objectFieldOffset方法获取了value这个字段值相对于AtomicInteger这个对象开始位置的内存偏移量。
我们可以来验证一下:
public class AtomicIntegerValueOffsetTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
AtomicInteger atomicInteger = new AtomicInteger();
Field valueOffset = atomicInteger.getClass().getDeclaredField("valueOffset");
valueOffset.setAccessible(true);
Long valueOffsetValue = (Long) valueOffset.get(atomicInteger);
System.out.println("valueOffset的值是"+valueOffsetValue);
System.out.println(ClassLayout.parseInstance(atomicInteger).toPrintable());
}
}
我们首先用反射获取了valueOffset的值,然后用ClassLayout输出了AtomicInteger这个对象的内存布局。ClassLayout是org.openjdk.jol包里面的类,用来解析对象的内存布局,关于ClassLayout的使用,可以参考这篇文章。看输出结果:
valueOffset的值是12
# 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
java.util.concurrent.atomic.AtomicInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) bb 3d 00 f8 (10111011 00111101 00000000 11111000) (-134201925)
12 4 int AtomicInteger.value 0
Instance size: 16 bytes
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()方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
compareAndSet方法在value的当前值等于期待值(expect参数的值)时,原子地将AtomicInteger的值设置为给定值(update参数的值)。
这个正是CMPXCHG这个指令的语义。它是通过unsafe.compareAndSwapInt这个方法实现的,这是一个native方法:
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()方法
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
这个方法我们应该经常会遇到,它保证了对value的原子自增操作。它调用的是unsafe的getAndAddInt方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
这个方法在一个持续的循环中,每次都是通过getIntVolatile方法取到value的当前值,然后使用compareAndSwapInt方法或者说LOCK CMPXCHG指令原子性地将这个当前值与比它大1的值交换,如果操作成功,就结束循环,如果没有,说明value已经被修改,就继续循环直到更新成功。这与上面说的compareAndSet()方法不同,compareAndSet()方法只会执行一次,不管是否更新成功。
更新不成功继续循环就是忙等待(忙循环)了,线程并不会被阻塞,而是一直在cpu上面执行。我们知道LOCK CMPXCHG只是一条指令而已,它执行完就是结束了,循环是软件(这里就是compareAndSwapInt这个方法)自己来实现的。
getIntVolatile方法通过当前AtomicInteger对象和valueOffset来获取value的当前值。为什么AtomicInteger中的value是volatile,这个我们会在将java内存模型时讲到,这里先不要重点关注它。
getAndAddInt()方法
这个方法与getAndIncrement方法类似,只是增加的不是1,而是一个参数指定的值。
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
它也是调用的unsafe.getAndAddInt()方法,同样会一直循环直到更新成功,这里不再重复。
Unsafe类的原子方法
Unsafe类中的compareAndSwapInt、compareAndSwapLong和compareAndSwapObject这三个native方法就是LOCK CMPXCHG这个原子指令在jdk中的映射:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
以compareAndSwapInt为例,它的前两个参数,用来进行内存位置的定位,也就是CMPXCHG命令中代表内存位置的的目的操作数,后面两个参数分别就是预期的旧值和要更新成的新值。
除了这三个方法,Unsafe还提供了getAndAddInt、getAndAddLong、getAndSetInt、getAndSetLong和getAndSetObject这五个方法来实现忙循环的原子更新,它们的实现方式都是在一个循环中去调用上面的三个方法,直到更新成功。以getAndAddInt为例,它的逻辑是这样的:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
它调用的就是compareAndSwapInt这个方法。
我们在上面AtomicInteger的讲解中已经看到了这些方法的使用,后面我们要介绍的AtomicObject和AtomicIntegerArray也会调用这些方法,这里我们梳理清楚了这些方法的逻辑,对理解之后的原子类会有帮助。
现在,我们用AtomicInteger来修改之前一个不是线程安全的代码:
public class UnsafeCount {
private static AtomicInteger count = new AtomicInteger();
public static void getNext(){
count.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<500;i++){
getNext();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<500;i++){
getNext();
}
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<500;i++){
getNext();
}
}
});
thread1.start();
thread2.start();
thread3.start();
//等待较长时间 让执行自增操作的线程完成
Thread.sleep(5000);
System.out.println(count);
}
}
在之前的版本中,count是一个int类型,我们改成了AtomicInteger,再次执行的结果总是1500。
小结
如果你理解了LOCK CMPXCHG命令,那么AtomicInteger类或者Unsafe类中的方法理解应该就很容易。LOCK CMPXCHG指令本质上就是原子性地根据预期值替换内存位置的值的操作,它并不是为自增设计的,但是AtomicInteger和Unsafe巧妙地利用这个功能,将它的更新值参数设置为value的当前值加1或者加某一个数,这样来实现自增1或者自增n的原子操作。