J.U.C 原子类系列之AtomicReference、Atom

作者: 爱打乒乓的程序员 | 来源:发表于2019-06-19 13:55 被阅读5次

    简介

    上一篇文章详细讲解了AtomicInteger原子类,还有和AtomicInteger原子类实现原理基本一样的AtomicLong和AtomicBoolean原子类。这些都是基本数据类型的原子类,在并发情景下可以保证基本数据类型变量的原子性。但是对于引用类型,这些基本类型的原子类就无能为力了,所以就出现对象引用类型的原子类

    对象引用类型的原子类包括:AtomicReference、AtomicStampedReference、AtomicMarkableReference

    AtomicReference原子类与基本数据类型的原子类实现过程相似,故不再赘述。

    不过值得注意的是,使用CAS会有ABA的隐患!什么是ABA?知乎用户对ABA相关的提问

    所以AtomicStampedReference、AtomicMarkableReference两个原子类就大派用场啦!

    AtomicStampedReference 的使用

    先看下AtomicStampedReference 原子类的核心源码:

    public class AtomicStampedReference<V> {
        // Pair对象维护对象的引用和对象标志的版本,通过Pair对象解决“ABA”问题
        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;
    
        public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); }
    
        public V getReference() { return pair.reference; }
    
        public int getStamp() { return pair.stamp; }
    
        public V get(int[] stampHolder) {
            Pair<V> pair = this.pair;
            stampHolder[0] = pair.stamp;
            return pair.reference;
        }
    
        public boolean weakCompareAndSet(V expectedReference,
                                         V newReference,
                                         int expectedStamp,
                                         int newStamp) {
            return compareAndSet(expectedReference, newReference,
                    expectedStamp, newStamp);
        }
    
        /**
         * @param expectedReference 期待的原始对象
         * @param newReference      将要更新的对象
         * @param expectedStamp     期待原始对象的标志版本
         * @param newStamp          将要更新对象的标志版本
         */
        public boolean compareAndSet(V expectedReference,
                                     V newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair<V> current = pair;
            return
                    expectedReference == current.reference && // 如果返回true则说明期待的原始对象与Pair的reference对象一样
                            expectedStamp == current.stamp &&  // 如果返回true说明期待原始对象标志版本与Pair的stamp对象一样
                            ((newReference == current.reference &&
                                    newStamp == current.stamp) || // 如果期待更新的对象和标志版本与Pair的reference和stamp一样的话直接返回true,否则执行CAS操作
                                    casPair(current, Pair.of(newReference, newStamp)));
        }
    
        public void set(V newReference, int newStamp) {
            Pair<V> current = pair;
            if (newReference != current.reference || newStamp != current.stamp)
                this.pair = Pair.of(newReference, newStamp);
        }
    
        public boolean attemptStamp(V expectedReference, int newStamp) {
            Pair<V> current = pair;
            return
                    expectedReference == current.reference &&
                            (newStamp == current.stamp ||
                                    casPair(current, Pair.of(expectedReference, newStamp)));
        }
    
        private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
        private static final long pairOffset =
                objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
    
        private boolean casPair(Pair<V> cmp, Pair<V> val) {
            return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
        }
    
        static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                      String field, Class<?> klazz) {
            try {
                return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
            } catch (NoSuchFieldException e) {
                NoSuchFieldError error = new NoSuchFieldError(field);
                error.initCause(e);
                throw error;
            }
        }
    }
    

    代码模拟 AtomicStampedReference 类解决“ABA”隐患

    AtomicStampedReference 实现的 CAS 方法增加版本号参数stamp,通过版本号就能够解决“ABA”问题。AtomicStampedReference类中大部分方法都可以根据方法名推测方法是有什么用。getXXX()方法无非就是从Pair对象属性中获取值;set方法将不同的对象或者不同的版本号设置给Pair对象。因此对AtomicStampedReference 的源码不作过多的解释。

    示例:线程A和线程B同时访问同一个值为1000的Integer类型对象。假设线程A执行CAS操作的时候,由于其它的原因还没将需要更新的值赋值,线程B抢先执行完CAS操作,并且将值为1000改为200,再改为1000(模拟ABA的情景),最后线程A才执行完CAS操作。

    ps.因为出现ABA的几率小,实现想不出怎么用代码实现,姑且的当线程A执行sleep()方法当作线程执行CAS方法过程遇到的一些阻碍吧~
    ABA例子
    public class AtomicStampedReferenceDemo {
    
        // 注意:如果引用类型是Long、Integer、Short、Byte、Character一定一定要注意值的缓存区间!
        // 比如Long、Integer、Short、Byte缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都new一个对象,那么即使两个对象的值相同,CAS方法都会返回false
        // 先声明初始值,修改后的值和临时的值是为了保证使用CAS方法不会因为对象不一样而返回false
        private static final Integer INIT_NUM = 1000;
        private static final Integer UPDATE_NUM = 100;
        private static final Integer TEM_NUM = 200;
    
        private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);
    
        public static void main(String[] args) {
            new Thread(() -> {
                int value = (int) atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(atomicStampedReference.compareAndSet(value, UPDATE_NUM, stamp, stamp + 1)){
                    System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
                }else{
                    System.out.println("版本号不同,更新失败!");
                }
            }, "线程A").start();
    
            new Thread(() -> {
                Thread.yield();// 确保线程A先执行
                int value = (int) atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
                atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), TEM_NUM, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
                atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), INIT_NUM, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
            }, "线程B").start();
        }
    }
    

    输出结果:

    线程A : 当前值为:1000 版本号为:1
    线程B : 当前值为:1000 版本号为:1
    线程B : 当前值为:200 版本号为:2
    线程B : 当前值为:200 版本号为:2
    版本号不同,更新失败!
    

    AtomicMarkableReference 的使用

    而AtomicMarkableReference原子类与AtomicStampedReference原子类源码实现相似,区别在于AtomicMarkableReference的标记是Boolean类型,只有两种状态true和false,适用在只需要知道
    AtomicMarkableReference对象是否有被修改的情景。

        // Pair对象维护对象的引用和对象标记
        private static class Pair<T> {
            final T reference;
            final boolean mark;// 通过标记的状态区分对象是否有更改
    
            private Pair(T reference, boolean mark) {
                this.reference = reference;
                this.mark = mark;
            }
    
            static <T> Pair<T> of(T reference, boolean mark) {
                return new Pair<T>(reference, mark);
            }
        }
        /**
         * @param expectedReference 期待的原始对象
         * @param newReference      将要更新的对象
         * @param expectedMark      期待原始对象的标记
         * @param newMark           将要更新对象的标记
         */
        public boolean compareAndSet(V expectedReference,
                                     V newReference,
                                     boolean expectedMark,
                                     boolean newMark) {
            Pair<V> current = pair;
            return
                    expectedReference == current.reference && // 如果期待的原始对象与Pair的reference一样则返回true
                            expectedMark == current.mark && // 如果期待的原始对象的标记与Pair的mark一样则返回true
                            ((newReference == current.reference &&
                                    newMark == current.mark) || // 如果要更新的对象和对象标记与Pair的refernce和mark一样的话直接返回true,否则执行CAS操作
                                    casPair(current, Pair.of(newReference, newMark)));
        }
    

    示例:依旧引用上面的例子

    public class AtomicMarkableReferenceDemo {
    
        private static final Integer INIT_NUM = 10;
    
        private static final Integer TEM_NUM = 20;
    
        private static final Integer UPDATE_NUM = 100;
    
        private static final Boolean INITIAL_MARK = Boolean.FALSE;
    
        private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(INIT_NUM, INITIAL_MARK);
    
        public static void main(String[] args) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " : 初始值为:" + INIT_NUM + " , 标记为: " + INITIAL_MARK);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (atomicMarkableReference.compareAndSet(INIT_NUM, UPDATE_NUM, atomicMarkableReference.isMarked(), Boolean.TRUE)) {
                    System.out.println(Thread.currentThread().getName() + " : 修改后的值为:" + atomicMarkableReference.getReference() + " , 标记为: " + atomicMarkableReference.isMarked());
                }else{
                    System.out.println(Thread.currentThread().getName() +  " CAS返回false");
                }
            }, "线程A").start();
    
            new Thread(() -> {
                Thread.yield();
                System.out.println(Thread.currentThread().getName() + " : 初始值为:" + atomicMarkableReference.getReference() + " , 标记为: " + INITIAL_MARK);
                atomicMarkableReference.compareAndSet(atomicMarkableReference.getReference(), TEM_NUM, atomicMarkableReference.isMarked(), Boolean.TRUE);
                System.out.println(Thread.currentThread().getName() + " : 修改后的值为:" + atomicMarkableReference.getReference() + " , 标记为: " + atomicMarkableReference.isMarked());
            }, "线程B").start();
        }
    }
    

    输出结果:

    线程A : 初始值为:10 , 标记为: false
    线程B : 初始值为:10 , 标记为: false
    线程B : 修改后的值为:20 , 标记为: true
    线程A CAS返回false
    

    由于线程B修改了对象,标记有false改为true,所以当上下文切换为线程A的时候,如果标记不一致CAS方法就会返回false。

    总结:

    日常开发中需要对象原子化操作可以使用AtomicReference、AtomicStampedReference和AtomicMarkableReference类;如果担心出现ABA隐患则从AtomicStampedReference和AtomicMarkableReference两个原子类中选取。

    参考资料:

    https://en.wikipedia.org/wiki/ABA_problem

    https://www.zhihu.com/question/23281499

    相关文章

      网友评论

        本文标题:J.U.C 原子类系列之AtomicReference、Atom

        本文链接:https://www.haomeiwen.com/subject/mrtkqctx.html