欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

并发编程之CAS(二)

程序员文章站 2022-05-04 12:51:25
...

更多Android架构进阶视频学习请点击:https://space.bilibili.com/474380680
本篇文章将从以下几个内容来阐述CAS:

  • [CAS原理]
  • [CAS带来的ABA问题]

一、什么是CAS?

在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)

JAVA1.5开始引入了CAS,主要代码都放在JUC的atomic包下,如下图:
并发编程之CAS(二)

二、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对其修改自然也就没有影响
更多Android架构进阶视频学习请点击:[https://space.bilibili.com/474380680]
参考:https://blog.csdn.net/u013887008/article/details/79784298
https://www.cnblogs.com/javalyy/p/8882172.html