juc-atomic原子类之四:AtomicReference原子类,CAS的ABA问题,AtomicStampedReference,AtomicMarkableReference

多个变量修改的原子性

JUC虽然提供了AtomicInteger、AtomicBoolean这些基本类型的原子类,但是有些场景并不是仅仅修改一个变量那么简单,有可能某个需要修改几个变量,但是需要这个操作具有原子性,比如说我给你举例的这个例子:

(1)假如有三个变量,value1、value2、value3,我需要他们都两两相等
(2)这时将value1、value2、value3都声明成AtomicInteger原子类
(3)定义一个线程类,创建两个线程实例,每个都执行5000次value1、value2、value3的操作
(4)每次操作完成之后对比value1、value2、value3是否两两相等,如果不满足,则打印报错。

public class MultiUpdateDemo {
    // 声明三个AtomicInteger的原子类
    private static AtomicInteger value1 = new AtomicInteger(0);
    private static AtomicInteger value2 = new AtomicInteger(0);
    private static AtomicInteger value3 = new AtomicInteger(0);
    // 定义一个线程,执行3个AtomicInteger的++操作
    public static class MultiUpdateThread extends Thread{
        @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 {
        // 创建两个线程,并发的操作
        MultiUpdateThread thread1 = new MultiUpdateThread();
        MultiUpdateThread thread2 = new MultiUpdateThread();

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

具体得到的实验结果如下:

 单独对value1、value2、value3中任意一个执行incrementAndGet是原子的;但是value1.incrementAndGet()、 value2.incrementAndGet()、value3.incrementAndGet()这三个操作合起来就不是原子的。可能thread1执行value1.incrementAndGet()操作的时候,thread2已经将三个自增操作执行完了,所以啊,thread1和thread2会相互干扰......

比如用锁也是可以的:

// 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的更新多个变量,确保原子性。

AtomicReference介绍和函数列表

AtomicReference是作用是对"对象"进行原子操作。

AtomicReference函数列表

// 使用 null 初始值创建新的 AtomicReference。
AtomicReference()
// 使用给定的初始值创建新的 AtomicReference。
AtomicReference(V initialValue)

// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
boolean compareAndSet(V expect, V update)
// 获取当前值。
V get()
// 以原子方式设置为给定值,并返回旧值。
V getAndSet(V newValue)
// 最终设置为给定值。
void lazySet(V newValue)
// 设置为给定值。
void set(V newValue)
// 返回当前值的字符串表示形式。
String toString()
// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
boolean weakCompareAndSet(V expect, V update)

AtomicReference源码分析(基于JDK1.8)

在JDK1.8中AtomicReference.java的源码如下:

public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = -1848883965231344442L;
    // 获取Unsafe对象,Unsafe的作用是提供CAS操作
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // volatile类型
    private volatile V value;

    public AtomicReference(V initialValue) {
        value = initialValue;
    }

    public AtomicReference() {
    }

    public final V get() {
        return value;
    }

    public final void set(V newValue) {
        value = newValue;
    }

    //最终设置为给定值
    public final void lazySet(V newValue) {
        unsafe.putOrderedObject(this, valueOffset, newValue);
    }

    //原子的修改值
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }

    //weakCompareAndSet操作仅保留了volatile自身变量的特性,而出去了happens-before规则带来的内存语义。
    //也就是说,weakCompareAndSet无法保证处理操作目标的volatile变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),
    //同时也无法保证这些变量的可见性.在jdk9中可体现
    public final boolean weakCompareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }


    @SuppressWarnings("unchecked")
    public final V getAndSet(V newValue) {
        return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
    }


    public final V getAndUpdate(UnaryOperator<V> updateFunction) {
        V prev, next;
        do {
            prev = get();
            next = updateFunction.apply(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }


    public final V updateAndGet(UnaryOperator<V> updateFunction) {
        V prev, next;
        do {
            prev = get();
            next = updateFunction.apply(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }


    public final V getAndAccumulate(V x,
                                    BinaryOperator<V> accumulatorFunction) {
        V prev, next;
        do {
            prev = get();
            next = accumulatorFunction.apply(prev, x);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    public final V accumulateAndGet(V x,
                                    BinaryOperator<V> accumulatorFunction) {
        V prev, next;
        do {
            prev = get();
            next = accumulatorFunction.apply(prev, x);
        } while (!compareAndSet(prev, next));
        return next;
    }


}

说明
AtomicReference的源码比较简单。它是通过"volatile"和"Unsafe提供的CAS函数实现"原子操作。
(1) value是volatile类型。这保证了:当某线程修改value的值时,其他线程看到的value值都是最新的value值,即修改之后的volatile的值。
(2) 通过CAS设置value。这保证了:当某线程池通过CAS函数(如compareAndSet函数)设置value时,它的操作是原子的,即线程在操作value时不会被中断。

三、AtomicReference底层原理

AtomicReference实现一个对象原子更新

package com.dxz;

import java.util.concurrent.atomic.AtomicReference;

public class ReferenceDemo {
    // 声明一个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;
    }

    // 创建线程累专门执行对象的更新
    public static class ReferenceThread extends Thread {
        @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 {
        // 创建两个线程,并发的操作,验证并发操作的原子性
        ReferenceThread thread1 = new ReferenceThread();
        ReferenceThread thread2 = new ReferenceThread();

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("运行结束了......");
    }
}

运行结果

C:\java\jdk1.8.0_111\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.2\lib\idea_rt.jar=31507:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.2\lib\idea_rt.jar" com.intellij.rt.execution.CommandLineWrapper C:\Users\4cv748wpd3\AppData\Local\Temp\idea_classpath82421684 com.dxz.ReferenceDemo
运行结束了......

Process finished with exit code 0

结果说明
并没有打印报错信息。它这里啊相当于把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替换操作。

结合源代码,跟Atomicinteger和AtomicBoolean原理是一样的是:

  • 一方面:成员变量是volatile修饰的V泛型变量。volatile保证了可见性和顺序性。
  • 另一方面:修改类的方法都是通过unsafe的cas实现,unsafte保证了原子性。

不同的是:只不过AtomicInteger、AtomicBoolean底层调用的是unsafe.compareAndSwapInt方法CAS操作int的值,而这里是compareAndSwapObject是CAS操作一个内存对象而已。

四、CAS的ABA问题

在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)这个时间差中,会导致数据的变化。ABA问题是这样子的:

如果待修改的值在CAS执行第二步的过程中被其他线程访问并修改,并且在第三步之前又修改为最初的值,例如先+1再-1。

 

 此时虽然线程A的CAS执行成功,但是值已经不是最初的值了。 

怎么避免ABA这种问题?

 这个应该是多增加一个维度,比如版本号,每一次修改数据版本号则递增1,然后执行CAS操作的时候多一个版本号维度判断,这样就能避免ABA问题了。

五、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多了一个版本号stamped,在执行CAS操作之前对比reference的值的同时也对比版本号,如果reference一样但是stamped不一样,说明期间有人修改过但是又把值改回来了,就不允许执行CAS操作了,这样就能解决ABA的问题了。

AtomicMarkableReference跟AtomicStampedReference差不多,

AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicStampedReference可能关心的是改动过几次,

AtomicMarkableReference的pair使用的是boolean mark,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。

posted on 2013-12-09 13:58  duanxz  阅读(477)  评论(0编辑  收藏  举报