先把JDK8的concurrent包中所有类截个图,先对他们名称啥的有个印象。
1 概要
学习atomic首先要弄清楚CAS操作,其次要对其中的各个类进行一个详细的分类。
下面我们把jdk8的java.util.concurrent.atomic中的所有类进行分组,方便接下来的学习:
- 基本类型(3个):AtomicBoolean、AtomicInteger、AtomicLong
- 基本类型数组(2个):AtomicIntegerArray、AtomicLongArray
- 更新字段(3个抽象类):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
- 引用类型(4个):AtomicReference、AtomicMarkableReference、AtomicStampedReference、AtomicReferenceArray
- 累加器(4个):DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder
- Striped64
共计17个类,下面我们将详细学习CAS和各分组的类。
2 CAS操作
引例:在执行i++;操作时由于不是原子操作,所以我们需将该复合操作进行加锁来保证该操作的原子性。一种方法是采用悲观锁策略synchronized关键字,另一种更高效的方法是采用硬件支撑的CAS(Compare-and-Swap)操作来实现。
CAS操作其实对应了硬件处理器底层的一个一个3操作数指令。在Java中该操作由sun.misc.Unsafe类里面的compareAndSwap***()提供支持,这一类方法是native方法,并且虚拟机对这类方法做了特殊处理,所以在编译期间将会编译成一条平台相关的CAS指令。
一条CAS的指令如下图所示(这里的操作数名字是我自己取得):
- 第一个操作数loc — 表示的是变量在主存中的地址(注意不是变量值本身,只有通过地址才能实时的观察到主存中变量的值,有的博文没有说明这一点,直接说成了该处存放的就是真实值)。
- 第二个操作数oldVal — 他表示我们上一次看到的主存中的变量值。这里后面解释的会更清晰。
- 第三个操作数newVal – 表示我们本次计算得到的新值。
执行一次CAS指令步骤:
首先根据loc取到当前主存中变量的值val,将该值和oldVal比较,如果val == oldVal 则认为当前线程得出新值的过程中变量的值没有(实际上还存一种例外的情况,该值被其他线程更新多次又回到原值,这就是ABA问题)被其他线程更新过,所以直接将newVal写回loc所指的主存变量val;如果val != oldVal,则证明在当前线程在得出新值newVal的过程中,主存中变量值被其他线程修改过,所以本次就不要将newVal写回主存覆盖val了。
那么我们是怎么利用CAS操作实现操作的同步的呢?我们还是以i++;自增为例,我们去看看AtomicInteger是如何实现自增操作的原子性的,先贴出AtomicInteger自增的代码片段:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
该方法实现的就是自增1操作,这里的this就是指我们继续跟进unsafe.getAndAddInt()方法,该方法是Unsafe类中的方法
public final int getAndAddInt(Object obj, long offset, int delta) {
int expect;
do {
// 取主存中变量值
expect= this.getIntVolatile(obj, offset);
// CAS操作
} while(!this.compareAndSwapInt(obj, offset, expect, expect + delta));
return expect; // 返回expect
}
public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
正如我们看到的,我们在do-while循环里,根据obj和offset拿到主存中当前的值expect,然后执行compareAndSwapInt方法,如果expect和旧值相比较没有发生变化,compareAndSwapInt方法就会把新值expect+delta写回主存,!this.compareAndSwapInt(obj, offset, expect, expect + delta)
就会返回false跳出循环。
这里值得注意的是在每次比较失败之后我们的旧值是会更新成本次获取到的主存中的值的。举个例子:假设有两个线程A和B。对于A线程,开始取出主存变量0到工作区,它自增0+1之后,想把1写回主存。它先看看主存中变量还是不是0,如果是0证明B线程没有修改该变量,我就放心把1写回主存。但是如果A观察到主存中的值已经为1了,证明B线程已经对变量的值修改了,这时A把1写回去就会丢失一次加1操作。所以此刻A不把1写回主存,而是再获取主存中的1,然后再执行一次自增1+1=2,自增后在看看主存中是1不?如果是1证明这次B线程总算没有在A自增时候对主存变量动手脚,所以A线程把2写回主存。
总结:getAndAddInt方法中,每次在想把新值写回主存时,都通过CAS操作观察主存中当前的值判断是否修改过,如果修改过就自旋,直到有一次观察到主存中的值没有被修改,才把本次更新的值写回主存。
AtomicInteger、AtomicLong、AtomicBoolean中的方法都与上述原理相似,就不一一说明了。
3 基本类型数组
AtomicIntegerArray、AtomicLongArray两个基本类型数组与基本类型的操作实现基本一致,只是多了对数组索引的操作。
以AtomicIntegerArray自增为例
// 根据数组下标i将相应位置值加1
public final int incrementAndGet(int i) {
return getAndAdd(i, 1) + 1;
}
public final int getAndAdd(int i, int delta) {
// 除了执行checkdByteOffset找到索引处的偏移,对该位置的Integer进行自增操作,与AtomicInteger是一样的
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
private long checkedByteOffset(int i) {
// 检查所给的索引是否合法
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
// 合法就返回基于第一个元素数组的偏移
return byteOffset(i);
}
private static long byteOffset(int i) {
return ((long) i << shift) + base;
}
4 字段更新
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater分别对volatile int、volatile long类型的字段进行原子更新,AtomicReferenceFieldUpdater对对应的引用类型字段volatile Integer、volatile Long进行原子更新。
public class AtomicExample5 {
// 通过newUpdater方法返回更新实例AtomicIntegerFieldUpdaterImpl引用
private static AtomicIntegerFieldUpdater<AtomicExample5> updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
public volatile int count = 100;
private static AtomicExample5 example5 = new AtomicExample5();
public static void main(String[] args){
System.out.println(updater.incrementAndGet(example5));
}
}
// 输出结果为 101