Java并发编程原理与实战四十三:CAS ---- ABA问题

Posted 屌丝码农

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程原理与实战四十三:CAS ---- ABA问题相关的知识,希望对你有一定的参考价值。

CAS(Compare And Swap)导致的ABA问题

问题描述

多线程情况下,每个线程使用CAS操作欲将数据A修改成B,当然我们只希望只有一个线程能够正确的修改数据,并且只修改一次。当并发的时候,其中一个线程已经将A成功的改成了B,但是在线程并发调度过程中尚未被调度,在这个期间,另外一个线程(不在并发中的请求线程)将B又修改成了A,那么原来并发中的线程又可以通过CAS操作将A改成B

测试用例:

public class AbaPro {

    private static final Random RANDOM = new Random();
    private static final String B = "B";
    private static final String A = "A";
    public static final AtomicReference<String> ATOMIC_REFERENCE = new AtomicReference<>(A);


    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch startLatch = new CountDownLatch(1);

        Thread[] threads = new Thread[20];
        for (int i=0; i < 20; i++){
            threads[i] = new Thread(){
                @Override
                public void run() {
                    String oldValue = ATOMIC_REFERENCE.get();
                    try {
                        startLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(RANDOM.nextInt()&500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    if (ATOMIC_REFERENCE.compareAndSet(oldValue, B )){
                        System.out.println(Thread.currentThread().getName()+ " 已经对原始值进行了修改,此时值为: "+ ATOMIC_REFERENCE.get());
                    }
                }
            };
            threads[i].start();
        }

        startLatch.countDown();
        Thread.sleep(200);

        new Thread(){

            @Override
            public void run() {
                try {
                    Thread.sleep(RANDOM.nextInt() & 200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String oldVal = ATOMIC_REFERENCE.get();
                while (!ATOMIC_REFERENCE.compareAndSet(ATOMIC_REFERENCE.get(), A));
                System.out.println(Thread.currentThread().getName() +" 已经将值 "+oldVal+" 修改成原始值: A");
            }

        }.start();
    }

}

结果:

Thread-12 已经对原始值进行了修改,此时值为: B
Thread-20 已经将值 B 修改成原始值: A
Thread-14 已经对原始值进行了修改,此时值为: B

可以看到并发中的线程Thread-12已经成功的将A修改成B,其他线程Thread-20在某一时刻将B修改成A,而并发中的线程Thread-14又能再次成功的将A修改成B,虽然最终结果是B,但是中途经历了一次被修改的过程,在某些情况下是致使的

解决方案

java中提供了AtomicStampedReference来解决这个问题,它是基于版本或者是一种状态,在修改的过程中不仅对比值,也同时会对比版本号

public class AabProResolve {

    private static final Random RANDOM = new Random();
    private static final String B = "B";
    private static final String A = "A";

    private static final AtomicStampedReference<String> ATOMIC_STAMPED_REFERENCE = new AtomicStampedReference<>(A,0);

    public static void main(String[] args) throws InterruptedException {

        final CountDownLatch startLatch = new CountDownLatch(1);
        Thread[] threads = new Thread[20];

        for (int i=0; i < 20; i++){
            threads[i] = new Thread(){

                @Override
                public void run() {
                    String oldValue = ATOMIC_STAMPED_REFERENCE.getReference();
                    int stamp = ATOMIC_STAMPED_REFERENCE.getStamp();

                    try {
                        startLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(RANDOM.nextInt() & 500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (ATOMIC_STAMPED_REFERENCE.compareAndSet(oldValue, B, stamp, stamp+1)){
                        System.out.println(Thread.currentThread().getName()+ " 已经对原始值: "+oldValue+" 进行了修改,此时值为: "+ ATOMIC_STAMPED_REFERENCE.getReference());
                    }
                }
            };
            threads[i].start();
        }
        Thread.sleep(200);
        startLatch.countDown();

        new Thread(){
            @Override
            public void run() {

                try {
                    Thread.sleep(RANDOM.nextInt() & 200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int stamp = ATOMIC_STAMPED_REFERENCE.getStamp();
                String oldVal = ATOMIC_STAMPED_REFERENCE.getReference();
                while (!ATOMIC_STAMPED_REFERENCE.compareAndSet(
                        B,
                        A,stamp, stamp+1)){
                    stamp = ATOMIC_STAMPED_REFERENCE.getStamp();
                }
                System.out.println(Thread.currentThread().getName() +" 已经将值 "+oldVal+" 修改成原始值: A");


            }
        }.start();

    }

}

结果:

Thread-1 已经对原始值: A 进行了修改,此时值为: B
Thread-20 已经将值 B 修改成原始值: A

可以看到并发期间的线程只有Thread-1对A进行了修改,保证了只有一个线程对数据的修改,短暂的并发时间之后的其他线程Thread-20对其修改自然也就没有影响、

 

优化方案,可以参考:

https://blog.csdn.net/wufaliang003/article/details/78797203

以上是关于Java并发编程原理与实战四十三:CAS ---- ABA问题的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程原理与实战四十二:锁与volatile的内存语义

Java并发编程原理与实战四十四:final域的内存语义

Java并发编程原理与实战十三:JDK提供的原子类原理与使用

Java并发编程原理与实战四十一:重排序 和 happens-before

Go并发编程之美-CAS操作

Java并发基石-CAS原理实战