【Java 并发】【八】【Atomic】【三】AtomicReference、AtomicStampReference原理
1 前言
上节我们看了AtomicInteger、AtomicBoolean的原理,这一篇我们就来说说Atomic系列的另一个分类AtomicReference和AtomicStampReference。
2 多个变量修改的原子性
JUC虽然提供了AtomicInteger、AtomicBoolean这些基本类型的原子类,但是啊有些场景并不是仅仅修改一个变量那么简单,有可能某个需要修改几个变量,但是需要这个操作具有原子性,比如:
(1)假如有三个变量,value1、value2、value3,我需要他们都两两相等
(2)这时将value1、value2、value3都声明成AtomicInteger原子类
(3)定义一个线程类,创建两个线程实例,每个都执行5000次value1、value2、value3的操作
(4)每次操作完成之后对比value1、value2、value3是否两两相等,如果不满足,则打印报错。
public class MultiUpdateDemo extends Thread { // 声明三个AtomicInteger的原子类 private static AtomicInteger value1 = new AtomicInteger(0); private static AtomicInteger value2 = new AtomicInteger(0); private static AtomicInteger value3 = new AtomicInteger(0); // 定义一个线程,执行3个AtomicInteger的++操作 @Override public void run() { for (int i = 0; i < 5000; i++) { value1.incrementAndGet(); value2.incrementAndGet(); value3.incrementAndGet(); // 假如说执行完一次操作之后,出现 // value1、value2、value3任何两两不相等的情况 // 则打印报错 if (value1.get() != value2.get() || value1 != value3 || value2.get() != value3.get()) { System.out.println("不好意思,出错了!!!!!!"); } } } public static void main(String[] args) throws InterruptedException { // 创建两个线程,并发的操作 MultiUpdateDemo demo1 = new MultiUpdateDemo(); MultiUpdateDemo demo2 = new MultiUpdateDemo(); // 启动两个线程 demo1.start(); demo2.start(); // 等待两个线程执行完 demo1.join(); demo2.join(); } }
其实原因大家应该能想到,单独对value1、value2、value3中任意一个执行incrementAndGet是原子的;但是value1.incrementAndGet()、 value2.incrementAndGet()、value3.incrementAndGet()这三个操作合起来就不是原子的,可能thread1执行value1.incrementAndGet()操作的时候,thread2已经将三个自增操作执行完了,所以啊,thread1和thread2会相互干扰。像是这种情况啊要对多个变量进行操作,同时又要保证这个操作具有原子性,单独使用AtomicInteger、AtomicBoolean是做不到的。
可能大家会想到:
// lock锁对象是一个共享变量 synchronized(lock) { value1.incrementAndGet(); value2.incrementAndGet(); value3.incrementAndGet(); // 加入说执行完一次操作之后,出现value1、value2、value3任何两两不相等的情况 if (value1.get() != value2.get() || value1 != value3 || value2.get() != value3.get()) { System.out.println("不好意思,出错了!!!!!!"); } }
这种情况下使用synchronized是可以保证原子性的,但是使用到锁啊,那并发性能就下降了很多了,因为在竞争激烈的时候可能会导致很多线程获取不到锁而挂起,那开销就大了,这个我们在之前的synchronized的重量级锁的时候分析过了。AtomicIntegter只能确保自己本身操作具有原子性,但是多个AtomicInteger操作合起来这个是确保不了的;可以使用synchronized将多个操作包含起来,但是使用到synchronized的锁操作势必会降低一部分并发的性能。
这个时候就需要用到Atomic给我们提供的另外一个类了,AtomicReference。它可以将多个变量封装为对象的多个属性,然后一次性的更新整个对象,就能cas的更新多个变量,确保原子性。
3 AtomicReference原理
AtomicReference实现一个对象原子更新,我们先把上边的例子改造一下:
public class MultiUpdateDemo extends Thread { // 声明一个AtomicReference,封装Demo对象的 private static AtomicReference<Demo> reference = new AtomicReference(new Demo()); // 将value1、value2、value3封装为Demo对象的属性 public static class Demo { public int value1 = 0; public int value2 = 0; public int value3 = 0; } // 定义一个线程,执行3个AtomicInteger的++操作 @Override public void run() { for (int i = 0; i < 5000; i++) { Demo expected; Demo update; // 直到CAS更新操作成功才退出 do { expected = reference.get(); update = new Demo(); update.value1 = expected.value1 + 1; update.value2 = expected.value2 + 1; update.value3 = expected.value2 + 1; } while (!reference.compareAndSet(expected, update)); // 获取CAS之后的最新对象 Demo curDemo = reference.get(); // 如果value1、value2、value3中有任意一个不相等,打印报错 if (curDemo.value1 != curDemo.value2 || curDemo.value2 != curDemo.value3 || curDemo.value1 != curDemo.value3) { System.out.println("不好意思,出错了!!!!!!"); } } } public static void main(String[] args) throws InterruptedException { // 创建两个线程,并发的操作 MultiUpdateDemo demo1 = new MultiUpdateDemo(); MultiUpdateDemo demo2 = new MultiUpdateDemo(); // 启动两个线程 demo1.start(); demo2.start(); // 等待两个线程执行完 demo1.join(); demo2.join(); } }
并没有打印报错信息。它这里啊相当于把value1、value2、value3的更新操作变为了对象的更新操作,这样原本的3次操作就变为了一次CAS操作,这样就能保证原子性了。多个数据变更的操作变为一个对象变更操作;由于AtomicReference提供了对象替换的CAS操作,所以上面的操作就具有原子性了。
画个图来解析它的步骤,就是这样的:
(1)将多个变量封装在一个对象中,比如demo对象,封装了value1、value2、value3变量的值,此时三个变量均为0
(2)此时要将3个变量的值均更新为1,则新创建一个对象update封装value1、value2、value3的值均为1
(3)此时只需要将旧的demo对象通过cas操作替换为新的update对象即可,这样就将多个变量的更新操作变为了一个对象的cas替换操作。
3.1 AtomicReference底层剖析
首先看一下AtomicReference的内部属性:
public class AtomicReference<V> implements java.io.Serializable { // unsafe对象 private static final Unsafe unsafe = Unsafe.getUnsafe(); // 一个泛型对象 private volatile V value; // value对象在AtomicReference内部的偏移量 private static final long valueOffset; static { try { // 获取value相对AtomicReference的内部偏移量 valueOffset = unsafe.objectFieldOffset (AtomicReference.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } }
看下compareAndSet方法的内部源码:
public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); }
看样子跟Atomicinteger和AtomicBoolean原理是一样的,只不过AtomicInteger、AtomicBoolean底层调用的是unsafe.compareAndSwapInt方法CAS操作int的值,而这里是compareAndSwapObject是CAS操作一个内存对象而已,没啥大区别。
AtomicInteger、AtomicBoolean 执行的是unsafe的compareAndSwapInt方法,在内存层次是直接替换一个int变量的值;然而使用AtomicRefernce你可以创建一个新的对象,将所有的数据变更操作放到新对象里面,然后底层调用unsafe.compareAndSwapObject方法直接替换成新对象。
3.2 CAS的ABA问题
CAS操作不可避免的问题之一就是ABA问题,我画个图说一下对ABA问题的理解:
(1)线程1要执行CAS操作前,读取value最新的值为A
(2)然后线程2在这期间将内存value的数据修改成B,然后又修改回了A;
(3)但是线程A不知道,执行CAS操作的时候发现值还是A,以为没人修改过value的值,也是就执行执行CAS操作成功了
那应该怎么避免ABA这种问题?这个应该是多增加一个维度,比如版本号,每一次修改数据版本号则递增1,然后执行CAS操作的时候多一个版本号维度判断,这样就能避免ABA问题了。
3.3 AtomicStampedReference原理
Atomic原子类系列里面有一个类叫做AtomicStampedReference,是AtomicReference的升级版本,看名字你就知道多了一个叫做Stamped的东西,这东西就是版本号,也叫作邮戳。下面让我们看看AtomicStampedReference的内部结构和核心方法。
AtomicStampedReference的内部结构:
public class AtomicStampedReference<V> { // 将当前对象引用和修改的版本号绑定成一个pair对 private static class Pair<T> { // 对象引用 final T reference; // 版本号 final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } private volatile Pair<V> pair; }
这里比较上面的AtomicReference多了一个stamp版本号,将对象和版本号绑定在一起,形成一对pair,比较的时候同时比较对象的引用和版本号,避免ABA问题。
核心执行修改的CAS方法:
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { // 这里获取当前的版本号和对象 Pair<V> current = pair; return // 这里对比对象是否被修改过,如果被修改过,则对象引用变化了 expectedReference == current.reference && // 比较版本号是否一致 expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
(1)获取旧的对象引用expectedRefenence
(2)执行CAS操作前,获取当前内存最新的数据
(3)对比旧的对象和当前对象的reference引用是否同一个,版本号stamp是否相同
(4)如果相同执行CAS操作替换,否则不一样说明有别的线程修改过数据,CAS操作失败
casPair方法:直接调用底层的unsafe类的compareAndSwapObject方法直接替换一个对象:
private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }
我们再画一张图来捋一捋:
其实就是比AtomicReference多了一个版本号stamp,在执行CAS操作之前对比reference的值的同时也对比版本号,如果reference一样但是stamp不一样,说明期间有人修改过但是又把值改回来了,就不允许执行CAS操作了,这样就能解决ABA的问题了。
4 小结
好了这节我们看了AtomicReference和AtomicStampReference,AtomicReference是用于修改多个属性的原子性操作,将要修改的属性封装进某个对象, 对整个对象进行CAS;AtomicStampReference是用于解决ABA问题,增加版本号的机制来实现,有理解不对的地方欢迎指正哈。