Java并发编程学习之如何解决CAS引起的ABA问题

  • 前言
  • 什么是CAS
  • ABA问题
  • 解决方案
  • 使用数据库乐观锁解决ABA问题
  • 使用AtomicStampedReference解决ABA问题
  • 使用AtomicMarkableReference解决ABA问题
  • 总结
  • 参考链接


前言

什么是CAS

  • 概念
    CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并交换
  • 实现原理
    CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B
    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B
  • 举例说明
  1. 在内存地址V当中,存储着值为10的变量。
  2. 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
  3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
  4. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败
  5. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋
  6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
  7. 线程1进行SWAP,把地址V的值替换为B,也就是12。
  • 存在问题
  1. CPU开销较大

并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

  1. 不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

  1. ABA问题

这是CAS机制最大的问题所在。

  • 其他
    CAS核心思想是采用乐观锁机制:它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新
    其中 java.util.concurrent 包下的 AtomicInteger 就是借助 CAS 来实现的。

ABA问题

  • 简介
    官方一点的解释就是:当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改原子类的值A成功,而这和原本设计目的相悖。
  • 测试样例
@Slf4j
    public class AtomicStampedReferenceTest extends SpringbootEasyexcelDemoApplicationTests {

        //线程操作资源,原子类ai的初始值为4
        public static final AtomicInteger ai = new AtomicInteger(4);

        @Test
        public void testABA(){

            new Thread(() -> {
                //利用CAS将ai的值改成5
                boolean b = ai.compareAndSet(4, 5);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为5:"+b);
                //休眠一秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //利用CAS将ai的值改回4
                b = ai.compareAndSet(5,4);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为4:"+b);
            },"A").start();

            new Thread(() -> {
                //模拟此线程执行较慢的情况
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //利用CAS将ai的值从4改为10
                boolean b = ai.compareAndSet(4, 10);
                System.out.println(Thread.currentThread().getName()+"是否成功将ai的值修改为10:"+b);
            },"B").start();

            //等待其他线程完成,为什么是2,因为一个是main线程,一个是后台的GC线程
            while (Thread.activeCount() > 2) {
                Thread.yield(); // 线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权
            }

            System.out.println("ai最终的值为:"+ai.get());
        }

    }
  • 结果分析

上面例子模拟的是A、B两个线程操作一个资源ai,A的执行速度比B的快,在B执行前,A就已经将ai的值改为5之后马上又把ai的值改回为4,但是B不感知,所以最后B就修改成功了。

  • 解决办法
    核心思路:加版本号

解决方案

使用数据库乐观锁解决ABA问题

  • 实现思路
    表中加一个VNO版本号字段,每次修改VNO = VNO + 1,则下一次修改则修改失败。

使用AtomicStampedReference解决ABA问题

  • 实现思路
    本质是有一个 int 值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并将版本号+1(当然加多少或减多少都是可以自己定义的),在zookeeper中保持数据的一致性也是用的这种方式。
  • 源码解析
public class AtomicStampedReference<V> {

        // 先看它的一个内部类Pair ,要进行原子操作的对象会被封装为Pair对象
        private static class Pair<T> {
            final T reference;  //要进行原子操作的对象
            final int stamp;    //当前的版本号
            private Pair(T reference, int stamp) {
                this.reference = reference;
                this.stamp = stamp;
            }

            //该静态方法会在AtomicStampedReference的构造方法中被调用,返回一个Pair对象;
            static <T> Pair<T> of(T reference, int stamp) { 
                return new Pair<T>(reference, stamp);
            }
        }

        // 它里面只有一个成员变量,要做原子更新的对象会被封装为Pair对象,并赋值给pair;
        private volatile Pair<V> pair;

        
        // 现在再看构造方法就明白了,就是将原子操作的对象封装为pair对象
        public AtomicStampedReference(V initialRef, int initialStamp) {
            pair = Pair.of(initialRef, initialStamp);
        }

 
        public V getReference() {
            return pair.reference;
        }

        // 获取版本号,就是返回成员变量pair的stamp的值    
        public int getStamp() {
            return pair.stamp;
        }

        /**
        * Returns the current values of both the reference and the stamp.
        * Typical usage is {@code int[1] holder; ref = v.get(holder); }.
        *
        * @param stampHolder an array of size of at least one.  On return,
        * {@code stampholder[0]} will hold the value of the stamp.
        * @return the current value of the reference
        */
        public V get(int[] stampHolder) {
            Pair<V> pair = this.pair;
            stampHolder[0] = pair.stamp;
            return pair.reference;
        }

        /**
        * Atomically sets the value of both the reference and stamp
        * to the given update values if the
        * current reference is {@code ==} to the expected reference
        * and the current stamp is equal to the expected stamp.
        *
        * <p><a href="package-summary.html#weakCompareAndSet">May fail
        * spuriously and does not provide ordering guarantees</a>, so is
        * only rarely an appropriate alternative to {@code compareAndSet}.
        *
        * @param expectedReference the expected value of the reference
        * @param newReference the new value for the reference
        * @param expectedStamp the expected value of the stamp
        * @param newStamp the new value for the stamp
        * @return {@code true} if successful
        */
        public boolean weakCompareAndSet(V   expectedReference,
                                        V   newReference,
                                        int expectedStamp,
                                        int newStamp) {
            return compareAndSet(expectedReference, newReference,
                                expectedStamp, newStamp);
        }

        /**
        * Atomically sets the value of both the reference and stamp
        * to the given update values if the
        * current reference is {@code ==} to the expected reference
        * and the current stamp is equal to the expected stamp.
        *
        * @param expectedReference the expected value of the reference
        * @param newReference the new value for the reference
        * @param expectedStamp the expected value of the stamp
        * @param newStamp the new value for the stamp
        * @return {@code true} if successful
        */

        /**
        * 原子修改操作,四个参数分别是旧的对象,将要修改的新的对象,原始的版本号,新的版本号。
        * 这个操作如果成功就会将expectedReference修改为newReference,将版本号expectedStamp修改为newStamp;
        */
        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)));
        }

        /**
        * Unconditionally sets the value of both the reference and stamp.
        *
        * @param newReference the new value for the reference
        * @param newStamp the new value for the stamp
        */
        public void set(V newReference, int newStamp) {
            Pair<V> current = pair;
            if (newReference != current.reference || newStamp != current.stamp)
                this.pair = Pair.of(newReference, newStamp);
        }

        /**
        * Atomically sets the value of the stamp to the given update value
        * if the current reference is {@code ==} to the expected
        * reference.  Any given invocation of this operation may fail
        * (return {@code false}) spuriously, but repeated invocation
        * when the current value holds the expected value and no other
        * thread is also attempting to set the value will eventually
        * succeed.
        *
        * @param expectedReference the expected value of the reference
        * @param newStamp the new value for the stamp
        * @return {@code true} if successful
        */
        public boolean attemptStamp(V expectedReference, int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                (newStamp == current.stamp ||
                casPair(current, Pair.of(expectedReference, newStamp)));
        }

        // Unsafe mechanics

        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) {
                // Convert Exception to corresponding Error
                NoSuchFieldError error = new NoSuchFieldError(field);
                error.initCause(e);
                throw error;
            }
        }
    }
  • 测试样例
@Slf4j
    public class AtomicStampedReferenceTest extends SpringbootEasyexcelDemoApplicationTests {

        public static final AtomicStampedReference<Integer> STAMPED_REFERENCE = new AtomicStampedReference<>(4,0);

        @Test
        public void testAtomicStampedReference(){

            new Thread(() -> {

                final int stamp = STAMPED_REFERENCE.getStamp();
                // 四个参数分别是预估内存值,更新值,预估版本号,初始版本号
                // 只有当预估内存值==实际内存值相等 并且 预估版本号==实际版本号,才会进行修改
                boolean b = STAMPED_REFERENCE.compareAndSet(4, 5,stamp,stamp + 1);

                System.out.println(Thread.currentThread().getName()+"是否成功将STAMPED_REFERENCE的值修改为5:"+ b + ",当前stamp值为" + STAMPED_REFERENCE.getStamp());

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                int stampAdd = STAMPED_REFERENCE.getStamp();
    //            System.out.println("当前stamp值为:" + stampAdd);

                b = STAMPED_REFERENCE.compareAndSet(5,4,stampAdd,stampAdd + 1);

                System.out.println(Thread.currentThread().getName()+"是否成功将STAMPED_REFERENCE的值修改为4:"+ b + ",当前stamp值为" + STAMPED_REFERENCE.getStamp());

            },"A").start();

            new Thread(() -> {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                boolean b = STAMPED_REFERENCE.compareAndSet(4, 10,0,1);

                System.out.println(Thread.currentThread().getName()+"是否成功将STAMPED_REFERENCE的值修改为10:"+ b + ",当前stamp值为" + STAMPED_REFERENCE.getStamp());

            },"B").start();

            while (Thread.activeCount() > 2) {
                Thread.yield();
            }

            System.out.println("STAMPED_REFERENCE最终的值为:"+STAMPED_REFERENCE.getReference() + ",标记stamp值为:" + STAMPED_REFERENCE.getStamp());

        }

    }
  • 结果分析

使用AtomicMarkableReference解决ABA问题

  • 实现思路
    AtomicMarkableReference则是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率
  • 测试样例
@Slf4j
    public class AtomicMarkableReferenceTest extends SpringbootEasyexcelDemoApplicationTests {

        /**AtomicMarkableReference 解决aba问题,注意,它并不能解决aba的问题 ,它是通过一个boolean来标记是否更改,本质就是只有true和false两种版本来回切换,只能降低aba问题发生的几率,并不能阻止aba问题的发生,看下面的例子**/
        public final static AtomicMarkableReference<String> ATOMIC_MARKABLE_REFERENCE = new AtomicMarkableReference<String>("abc" , false);

        @Test
        public void testAtomicMarkableReference(){


            new Thread(() -> {

                //线程A 获取到mark状态为false,原始值为“abc”
                boolean marked = ATOMIC_MARKABLE_REFERENCE.isMarked();

                // 四个参数分别是预估内存值,更新值,预估标记,初始标记
                // 如果当前引用Reference 和 expectedReference相同,并且当前标记mark值和期望mark值相同,则原子更新引用和标记为新值newReference 和 newMark
                boolean b = ATOMIC_MARKABLE_REFERENCE.compareAndSet("abc", "aba",marked,!marked);

                System.out.println(Thread.currentThread().getName()+"是否成功将ATOMIC_MARKABLE_REFERENCE的值修改为aba:"+ b + ",当前marked值为" + ATOMIC_MARKABLE_REFERENCE.isMarked());

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                boolean markedNew = ATOMIC_MARKABLE_REFERENCE.isMarked(); // true
    //            System.out.println("当前marked值为:" + markedNew);

                b = ATOMIC_MARKABLE_REFERENCE.compareAndSet("aba","abc",markedNew,!markedNew);

                System.out.println(Thread.currentThread().getName()+"是否成功将ATOMIC_MARKABLE_REFERENCE的值修改为abc:"+ b + ",当前marked值为" + ATOMIC_MARKABLE_REFERENCE.isMarked());

            },"A").start();


            new Thread(() -> {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                boolean b = ATOMIC_MARKABLE_REFERENCE.compareAndSet("abc", "abb",false,true);

                System.out.println(Thread.currentThread().getName()+"是否成功将ATOMIC_MARKABLE_REFERENCE的值修改为abb:"+ b + ",当前marked值为" + ATOMIC_MARKABLE_REFERENCE.isMarked());

            },"B").start();

            while (Thread.activeCount() > 2) {
                Thread.yield();
            }

            System.out.println("ATOMIC_MARKABLE_REFERENCE最终的值为:"+ATOMIC_MARKABLE_REFERENCE.getReference() + ",标记marked值为:" + ATOMIC_MARKABLE_REFERENCE.isMarked());

        }

    }
  • 结果分析

按照上面的执行顺序,3次修改都修改成功了,线程1做的操作是将“abc”更改为“aba”,再改回“abc”, 线程2在线程1更改结束后做了一次更改为“abb”,最后线程2做操作的时候并没有感知到线程1的第二次更改

总结

  • CAS的比较过程不加锁为什么实现的是原子操作
    CAS的加锁过程是系统级的,CPU的单元化实现的,不会出现时间片拆分,所以CAS的比较修改是原子操作
  • CAS核心类
    Unsafe类是CAS核心类
    Java无法直接访问底层操作系统,是通过本地native方法来进行访问。jvm还是提供了一个后门,这个类提供了硬件级别的原子操作
    valueOff表示的是变量值在内存中的偏移地址,因为unsafe就是根据内存偏移地址获取数据的原值的,这样我们就可以通过unsafe实现cas了