zoukankan      html  css  js  c++  java
  • CAS-原子操作

    一  概念

        在计算机中比较和交换(Compare And Swap  即 CAS)是用于实现多线程同步的原子指令,当多个线程对某个资源进行CAS操作,只能有1个线程成功,其他线程并不会阻塞而是返回失败信号。

    二  基本原理

    每个CAS包含3个运算符
    1 内存地址 V
    2 旧的预期值A
    3 新值B

    基本思路,如果V地址上的值和传入的旧的期望值A相等,就给地址赋个新值B,否则不做任何处理

    if(V.value.equals(A))
    {
        V.value = B;
        return true;
    }else
    {
        return false;
    }

    为了确保CAS操作的成功,会使用死循环内执行CAS,直到其成功为止

    常用 原子操作类型
    基本类型 AtomicInteger,AtomicLong,AtomicBoolean;
    数组类型 AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray;
    引用类型 AtomicReference,AtomicMarkableReference,AtomicStampedReference


    三 常见用法和ABA问题

    3.1 常见用法

    原子操作基本类型(Integer)
    //integer 原子操作对象
    AtomicInteger atomicInteger = new AtomicInteger(10);
    
    //基本类型原子操作测试
    public  void atomicIntegerTest()
    {
        //自增1 并返回新值
        System.out.println("自增 并返回新值="+atomicInteger.incrementAndGet());
    
        //返回旧值,再加1
        System.out.println("返回 ="+ atomicInteger.getAndIncrement()+" 并自增");
    }

     运行结果 

    自增 并返回新值=11
    返回=11 并自增

    用途 比如可以用于多线程的计数等,

    如下面incrementAndGet()的源码内部,就是使用了死循环,用CAS去增加值,一直到成功为止
    //incrementAndGet() 的部分源码
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    }

    我们也可以自己实现自己的 incrementAndGet()方法,暂命名为 myIncrementAndGet()
    //自己的 incrementAndGet 方法  自增1并获取到新值
    public int myIncrementAndGet(){
    //循环更新,直到CAS成功
    for (; ; ) {
        int current = atomicInteger.get();
        int newValue = current + 1;
    
        if (atomicInteger.compareAndSet(current, newValue)) {
            return newValue;
        }
    }
    }
    
    
    原子操作类型数组
    //数组
    int [] arr = new int[]{1,2,3,4,5,6,7,8};
    
    //数组类型原子操作对象
    private  AtomicIntegerArray atomicArray = new AtomicIntegerArray(arr);
    
    @Test
    public  void test()
    {
        //获取索引为0的值,并修改索引0里的值为3
        System.out.println(atomicArray.getAndSet(0,3));
        System.out.println("原数组索引0的值="+arr[0]);
        System.out.println("原子类型数组里索引0的值="+atomicArray.get(0));
    }
    运行结果
    1
    原数组索引0的值=1
    原子类型数组里索引0的值 = 3;


    3.2 ABA问题

    原来V地址上值为A

    1. 时刻1 线程1 执行读取操作,获取原值A,然后线程被切换走了
    2. 时刻2 线程2 更新了V上的值为B
    3. 时刻3 线程2 更新了V上的值为A
    4. 时刻4 线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等(都是A)。
    然后用新值(newValue)B 写入内存中,完成 CAS 操作

    我们可以发现,其实V值,在线程1修改之前,已经被修改过2次了(改为B,又改回A)
    但线程1并不知道原值被修改,所以继续执行流程。

    处理ABA问题

          通常是对每一次CAS操作设置版本戳,目前java.util.concurrent.atomic包下提供2种原子类

    类名 AtomicMarkableReference    AtomicStampedReference
    版本戳特点 版本戳是 boolean 类型    版本戳是number类型   
    数据 只关心数据是否被动过      关心数据被动过几次

                                                              

                                                                                                                          

                                                                                 

    看具体示例

    public class AtomicStampedRefernceTest {
        //带版本戳的原子操作
        private static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("作者", 1);
        //旧的版本戳
        final int oldstamp = atomicStampedReference.getStamp();
        //旧值
        final String oldValue = atomicStampedReference.getReference();
    
        @Test
        public void mainTest() {
            Thread right = new Thread(new rightThread());
            //正确版本号线程
    
            //错误版本号线程
            Thread error =  new Thread(new errorThread());
    
            try {
                right.start();
                right.join();
                error.start();
                error.join();
            }catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
    
        }
    
        //正确使用了版本号进行了CAS的线程
        public class rightThread implements Runnable {
            @Override
            public void run() {
                String value = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
    
                Thread.currentThread().setName("正确使用了版本号线程");
                System.out.println(Thread.currentThread().getName() + ":值=" + value + ",版本戳=" + stamp + " CAS结果=" +
                        atomicStampedReference.compareAndSet(value, value + "-帅哥", stamp, stamp + 1));
            }
        }
    
        //错误使用了版本号进行了CAS的线程
        public class errorThread implements Runnable {
            @Override
            public void run() {
                String value = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                Thread.currentThread().setName("错误使用了版本号线程");
                System.out.println(Thread.currentThread().getName() + ":值=" + value + ",版本戳=" + stamp + " CAS结果=" +
                        atomicStampedReference.compareAndSet(value, value + "-老者", oldstamp, oldstamp + 1));
            }
        }
    }
    输出结果

    正确使用了版本号线程:值=作者,版本戳=1 CAS结果=true
    错误使用了版本号线程:值=作者-帅哥,版本戳=2 CAS结果=false

    总结:

    CAS和synchronized有什么区别?

        synchronized是java的内置锁,目的是用于锁住某块资源,确保这块资源只能被一个线程访问(即确保资源的原子性),是悲观锁。
        CAS是计算机指令级别的一个原子操作

    使用synchronized 有什么不方便的地方或者弊端?
        1 synchronized 是基于阻塞机制,如果某个线程拿不到锁会阻塞在那里,当被阻塞的线程优先级很高时,我们无法中断
        2 拿到锁的线程一直不释放怎么办
        3 大量竞争锁,会出现死锁或活锁,而且会消耗CPU,以及其他一些安全问题

    使用CAS的弊端
        为了确保CAS操作成功,你要死循环调用,直到成功为止,造成CPU开销问题

    什么情况下用CAS?
        当觉得用锁不方便,或者太笨重时,可以考虑用CAS

  • 相关阅读:
    Spring-四种常用注解
    Spring-IOC
    Spring-bean 的管理细节
    Android R.java文件
    MySQL | windows10 安装MySQL : 无法将“mysql”项识别为 cmdlet、函数、脚本....
    统计知识 | 决定系数 R方、调整后的R方、F值
    R | R语言表达式中常用的符号
    回归分析 | R语言回归算法、模型诊断
    R | 探索性数据分析 EDA
    R语言基础 | 概率分布的表示方法
  • 原文地址:https://www.cnblogs.com/hup666/p/11871403.html
Copyright © 2011-2022 走看看