文章浅谈volatile的最后留下了疑问,为什么AtomicInteger能保证原子性,AtomicInteger是如何做到保证原子性的,本篇文章就是来答疑解惑的。
AtomicInteger源码分析
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
分析:这是文章浅谈volatile里所使用的方法源码,可以看到,源码是使用Unsafe类的getAndAddInt()来实现的。那么Unsafe类又是什么样的呢?下图是Unsafe类的部分截图:
通观Unsafe类的方法,其返回值都是native,也就是c语言的函数,而getAndAddInt()的源码如下图所示:
源码解析:var1 = this,也就是当前对象,如:num.getAndIncrement();
中,this就是num,var2 = valueOffset,也就是偏移量,通过this和偏移量可以准确的在内存中找到改对象的值,也就是var5,var4 = 1,函数getAndIncrement()每次增加1,所以这里是写死的1,compareAndSwapInt(var1, var2, var5, var5 + var4)
是CAS方法,含义:比较this的内存值与当前从内存中取出的值var5是否相同,相同则把内存中的值更新为var5+var4(新值),不相同则继续while()循环,直到比较的结果相同为止。
CAS介绍
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是和原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令
。这是一种完全依赖于硬件的功能,通过它实现了原子操作,不会造成数据不一致问题。
CAS示例
AtomicInteger atomicInteger = new AtomicInteger(2019);
System.out.println("CAS boolean:" + atomicInteger.compareAndSet(2019, 2020) + ",CAS value:" + atomicInteger.get());
System.out.println("CAS boolean:" + atomicInteger.compareAndSet(2019, 2021) + ",CAS value:" + atomicInteger.get());
输出结果:
结果分析:第一次CAS操作时,期望值与实际值相同,所以更新成功,atomicInteger的值修改为:2020,第二次CAS操作时,期望值(2019)与实际值(2020)不相同,所以比对不成功,也就不会更新(2021)了。
到此其实CAS原理就解释完毕了,但是并没有介绍CAS的缺点,所以接下来会介绍CAS的缺点,以及CAS带来的问题和如何解决问题,帮助更好的理解CAS。
CAS缺点
- 循环时间长,开销大
- 只能保证一个共享变量的操作
- ABA问题
解释:CAS的实现是do-while(),如果某个或者某些线程运气不好,在更新值前,一直被其他线程抢先,那么while()循环会一直执行(自旋),可能对CPU操作开销影响。只能保证一个共享变量这一点很好理解,ABA问题会详细介绍。
ABA问题
CAS比较的时候,是通过期望值和当前值进行比较,如果相等,认为没有被修改,则进行自己的操作,那么当出现,当前值A,某线程X将其修改为B,然后线程X又把当前值改回A,对于另一个线程Y而言,自己获取到的当前值是A,期望值也是A,线程Y认为值A没有发生过变化。实质:只关心首尾相同,中间或许会有变化。A—>B—>…—>A
代码示例:
public class T {
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
System.out.println("---ABA问题---");
System.out.println("原始值:" + atomicInteger.get());
//0-->1-->0
new Thread(() -> {
atomicInteger.compareAndSet(0, 1);
System.out.println("步骤一:" + atomicInteger.get());
try {
//线程A休眠1秒,保证线程A按照0-->1-->0执行
TimeUnit.SECONDS.sleep(1);
atomicInteger.compareAndSet(1, 0);
System.out.println("步骤二:" + atomicInteger.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread A").start();
new Thread(() -> {
//线程B休眠3秒,保证线程A完成ABA问题
try {
TimeUnit.SECONDS.sleep(3);
atomicInteger.compareAndSet(0, 2);
System.out.println("步骤三:" + atomicInteger.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread B").start();
while (Thread.activeCount() > 2) {}
System.out.println("最终结果:" + atomicInteger.get());
}
}
输出结果:
通过对ABA问题的理解,结合代码以及输出,可以清楚的看到,中间步骤有变化,但是没有被感知到。
ABA问题解决方案
了解了ABA问题的原因,就比较好给出解决方案了,加上时间戳(版本号),使得每次修改,版本号都变化,那么线程Y在修改当前值的时候,比对版本号,就会发现,版本号不一致,那么自己的值就一定需要重新获取了,在给出解决方案之前,我们先来看看原子引用。
原子引用
public class T {
public static void main(String[] args) {
Student student1 = new Student("kevin", 31);
Student student2 = new Student("james", 33);
AtomicReference<Student> atomicReference = new AtomicReference<>();
atomicReference.set(student1);
System.out.println("原始值:" + atomicReference.get().toString());
atomicReference.compareAndSet(student1, student2);
System.out.println("结果值:" + atomicReference.get().toString());
}
}
输出结果:
结果分析:通过原子引用类型,我们可以设置任意的原子类型,例子中的是Student类,实际需要的或许是People类或者是其他类。了解了原子引用后,我们再看ABA问题解决方案代码,就更容易理解了。
ABA问题解决方案代码示例:
public class T {
//初始值为0,版本号为1
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(0, 1);
public static void main(String[] args) {
System.out.println("---ABA问题解决---");
System.out.println("原始值:" + atomicStampedReference.getReference());
//0-->1-->0
new Thread(() -> {
atomicStampedReference.compareAndSet(0, 1, 1, 2);
System.out.println("步骤一值:" + atomicStampedReference.getReference() + "版本号:" + atomicStampedReference.getStamp());
try {
//线程A休眠1秒,保证线程A按照0-->1-->0执行
TimeUnit.SECONDS.sleep(1);
atomicStampedReference.compareAndSet(1, 0, 2, 3);
System.out.println("步骤二值:" + atomicStampedReference.getReference() + "版本号:" + atomicStampedReference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread A").start();
new Thread(() -> {
//线程B休眠3秒,保证线程A完成ABA问题
try {
TimeUnit.SECONDS.sleep(3);
atomicStampedReference.compareAndSet(0, 1, 1, 2);
System.out.println("步骤三值:" + atomicStampedReference.getReference() + "版本号:" + atomicStampedReference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread B").start();
while (Thread.activeCount() > 2) { }
System.out.println("最终结果值:" + atomicStampedReference.getReference());
}
输出结果:
对比ABA问题的输出结果,可以看到,有了版本号后,最终的结果没有被修改成功,原因就在于版本号没有比对成功。代码中给出的原子时间戳引用的类型是Integer(AtomicStampedReference<Integer>
),泛型的参数可以是任意的。