并发-CAS 原理浅解01

Posted YangXueChina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发-CAS 原理浅解01相关的知识,希望对你有一定的参考价值。

CAS-并发编程的基石

在开发的过程中,经常有并发场景。就需要从底层了解实现策略及方式,因此有了本文。

引入

需求:我们开发一个网站,需要对访问量进行统计,用户每发送一次请求,访问量+1,如何实现?
我们模拟有100个人同时访问,并且每个人对咱们的网站发起10次请求,最后总访问次数应该是1000次。

小试牛刀,下面的demo try一下

public class Demo 

    //总访问量
    static int count = 0;

    //模拟访问的方法
    public static void request() throws InterruptedException 
        //模拟耗时5s
        TimeUnit.MILLISECONDS.sleep(5);
        /**
         *  main,耗时:63 ,count = 964
         * 
         * Q: count值不对分析问题出在哪?
         * A: count++操作,实际是3步执行完成(jvm执行引擎)
         *     1。获取count的值,记A :A=count
         *     2。将A值+1,得到B:B=A+1
         *     3。将B值赋值给count:count=B
         *
         *      case: 如果有A、B两个线程,第一步时,拿到的count值是一样的,第二步操作时都+1,那么到第三步结束
         *      时count的值只+1,导致count的值不正确(应该正常是要+2)
         *
         *
         *
         *  Q:怎么解决不正确的问题
         *  A:对count++操作的时候,让多个线程排队处理,多个线程同时到达request()方法的时候,只能允许一个线程可以进去操作,
         *  其他的线程在外面等待,等里面的处理完毕之后,外面等着的再进去一个,这样操作的count++是排队进行的,count一定正确
         *
         *  Q:怎么实现队列效果
         *  A:Java中的synchronized关键字和ReentrantLock都可以实现对资源加锁,保证并发的正确性,多线程的情况下可以保证被锁住
         *  的资源被串行访问。
         *
         *
         */
        count ++;
    

    public static void main(String[] args) throws InterruptedException 
        //开始时间
        long startTIme = System.currentTimeMillis();
        //模拟用户 100个用户
        int threadSize = 100;

        CountDownLatch countDownLatch = new CountDownLatch(threadSize);

        //开启100个线程
        for (int i = 0; i < threadSize; i++) 
            //创建线程
            Thread thread = new Thread(new Runnable() 
                @Override
                public void run() 
                    //模拟用户行为,每个用户访问10次
                    try 
                        for (int j = 0; j < 10; j++) 
                            request();
                        
                     catch (Exception e) 
                        e.printStackTrace();
                     finally 
                        //减1操作
                        countDownLatch.countDown();
                    
                
            );
            //执行线程,调用run方法
            thread.start();
        
        //怎么在100个线程 结束之后,再执行后面代码 -- CountDownLatch
        //阻塞等待,上面都执行完再执行下面的代码
        countDownLatch.await();

        //结束时间
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - startTIme) + " ,count = " + count);
    

很不理想,每次运行的返回值总是差一点点,差一点点

main,耗时:63 ,count = 964

统揽代码,可以看到是因为count变量的操作,count++是三步走实现的,在并发场景下产生了问题:

  	  1。获取count的值,记A :A=count
      2。将A值+1,得到B:B=A+1
      3。将B值赋值给count:count=B

解决方案一:synchronized

public class Demo02 

    //总访问量
    static int count = 0;

    /**
     * Q:(main,耗时:5736 ,count = 1000)耗时太长的原因是什么?
     * A:程序中对request()方法使用synchronized关键字,保证了并发场景下,request方法同一时刻只允许一个线程
     * 进入,request加锁相当于串行执行了,count的结果与预期一致,但是耗时太长。。。
     *
     * Q:如何解决耗时长的问题
     * A: count++操作,实际是3步执行完成(jvm执行引擎)
     *     1。获取count的值,记A :A=count
     *     2。将A值+1,得到B:B=A+1
     *     3。将B值赋值给count:count=B
     *  升级第3步的实现
     *      1、获取锁
     *      2、获取一下count的最新的值,记做LV (最新值)
     *      3、判断LV的值是否等于A,如果相等,则将B的值赋值给count,并返回true,否则返回false
     *      4、释放锁
     *
     */
    public **synchronized** static void request() throws InterruptedException 
        //模拟耗时5s
        TimeUnit.MILLISECONDS.sleep(5);
        count ++;
    

    public static void main(String[] args) throws InterruptedException 
        //开始时间
        long startTIme = System.currentTimeMillis();
        //模拟用户 100个用户
        int threadSize = 100;

        CountDownLatch countDownLatch = new CountDownLatch(threadSize);

        //开启100个线程
        for (int i = 0; i < threadSize; i++) 
            //创建线程
            Thread thread = new Thread(new Runnable() 
                @Override
                public void run() 
                    //模拟用户行为,每个用户访问10次
                    try 
                        for (int j = 0; j < 10; j++) 
                            request();
                        
                     catch (Exception e) 
                        e.printStackTrace();
                     finally 
                        //减1操作
                        countDownLatch.countDown();
                    
                
            );
            //执行线程,调用run方法
            thread.start();
        
        //怎么在100个线程 结束之后,再执行后面代码 -- CountDownLatch
        //阻塞等待,上面都执行完再执行下面的代码
        countDownLatch.await();

        //结束时间
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - startTIme) + " ,count = " + count);
    


在request()方法增加了synchronized修饰符,来看下结果

main,耗时:5736 ,count = 1000

amazing ,比较可怕,耗时激增,这不是我们想要的结果。非常影响性能。

解决方案二:锁范围减小

CAS因运而生

   * 升级第3步的实现
     * 1、获取锁
     * 2、获取一下count的最新的值,记做LV (最新值)
     * 3、判断LV的值是否等于A,如果相等,则将B的值赋值给count,并返回true,否则返回false
     * 4、释放锁

volatile+ compareSwap

public class Demo03 
    //总访问量
    **volatile** static int count = 0;
    public static void request() throws InterruptedException 
        //模拟耗时5s
        TimeUnit.MILLISECONDS.sleep(5);
        
        **int expectCount;//表示期望值
        //CAS循环操作的实现,每次request请求都会进行循环(自旋),compareSwap方法返回true时,循环结束
        while (!compareSwap(expectCount = getCount(), expectCount + 1)) 
        **
    

    /**
     * CAS
     * @param expectCount 期望值count
     * @param newCount    最新值count
     * @return 成功返回true 失败返回false
     */
    public static synchronized boolean compareSwap(int expectCount, int newCount) 
        //判断count当前值是否和期望值expectCount一致,若一致将newCount赋值给count
        if (getCount() == expectCount) 
            count = newCount;
            return true;
        
        return false;
    

    public static int getCount() 
        return count;
    


    public static void main(String[] args) throws InterruptedException 
        //开始时间
        long startTIme = System.currentTimeMillis();
        //模拟用户 100个用户
        int threadSize = 100;

        CountDownLatch countDownLatch = new CountDownLatch(threadSize);

        //开启100个线程
        for (int i = 0; i < threadSize; i++) 
            //创建线程
            Thread thread = new Thread(new Runnable() 
                @Override
                public void run() 
                    //模拟用户行为,每个用户访问10次
                    try 
                        for (int j = 0; j < 10; j++) 
                            request();
                        
                     catch (Exception e) 
                        e.printStackTrace();
                     finally 
                        //减1操作
                        countDownLatch.countDown();
                    
                
            );
            //执行线程,调用run方法
            thread.start();
        
        //怎么在100个线程 结束之后,再执行后面代码 -- CountDownLatch
        //阻塞等待,上面都执行完再执行下面的代码
        countDownLatch.await();

        //结束时间
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - startTIme) + " ,count = " + count);
    


CAS

Compare And Swap 比较并交换
上面的方案二,相当于手撕了一遍CAS。其原理大致就是,使用一个volatile变量记录(其作用是,多线程之间修改立即可见)
CAS操作有三个元素:内存值-即旧值 V,期望值 E,新值N
CAS(V,E,N),期望值与内存值进行比较,相等的话将内存值更新为N,不相等则进行自循。
CAS在解决一下问题的时候,本身也存在一些问题:

ABA问题

ABA,解释一下就是将原A值更新为B,又将B更新为A。狸猫换太子

所谓ABA问题,其实用最通俗易懂的话语来总结就是狸猫换太子
就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。

比如有两个线程A、B:

  1. 一开始都从主内存中拷贝了原值为3;
  2. A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起;
  3. B修改原值为4,B线程执行完毕;
  4. 然后B觉得修改错了,然后再重新把值修改为3;
  5. A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值等于快照值3,(但是却不知道B曾经修改过),修改成功。

尽管线程A CAS操作成功,但不代表就没有问题。有的需求,比如CAS,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改。这就引出了AtomicReference原子引用。

AtomicReference

原子引用

AtomicStampedReference和ABA问题的解决:

使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个“版本号”Stamp,其实有点类似乐观锁的意思。
在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作.

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);
 public static AtomicStampedReference<Integer> a = new AtomicStampedReference(new Integer(1), 1);

    /**
     * 操作线程主线程, 初始值:1
     * 操作线程干扰线程,【increment】,值=2
     * 操作线程干扰线程,【decrement】,值=1
     * 操作线程主线程,CAS操作:true
     *
     */
    public static void main(String[] args) 
        //主线程
        Thread main = new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("操作线程" + Thread.currentThread().getName() + ", 初始值:" + a.getReference());
                try 
                    // 期望值的引用
                    Integer expectReference  = a.getReference();
                    // 新值的引用
                    int newReference = expectReference + 1;
                    // 期望引用的版本号
                    int expectStamp = a.getStamp();
                    // 新值的版本号
                    int newStamp = expectStamp + 1;

//                    int expectNum = a.get();
//                    int newNum = expectNum + 1;
                    //主线程休眠一秒钟,让出cpu
                    Thread.sleep(1000);

                    //CAS操作
                    boolean isCASSccuess = a.compareAndSet(expectReference, newReference,expectStamp,newStamp);

                    System.out.println("操作线程" + Thread.currentThread().getName() + ",CAS操作:" + isCASSccuess);

                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        , "主线程");

        //干扰线程
        Thread other = new Thread(new Runnable() 
            @Override
            public void run() 
                try 
                    //确保Thread-main线程优先执行
                    Thread.sleep(20);

                    a.compareAndSet(a.getReference(), (a.getReference() + 1), a.getStamp(), (a.getStamp() + 1));
                    System.out.println("操作线程" + Thread.currentThread().getName() + ",【increment】,值=" +a.getReference());


                    a.compareAndSet(a.getReference(), (a.getReference() - 1), a.getStamp(), (a.getStamp() + 1));
                    System.out.println("操作线程" + Thread.currentThread().getName() + ",【decrement】,值=" +a.getReference());

                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        , "干扰线程");

        main.start();
        other.start();
    

有趣例子:
关于ABA问题一个例子:在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。解决这个问题的方案的一个策略是每一次倒水假设有一个自动记录仪记录下,这样主人回来就可以分辨在她离开后是否发生过重新倒满的情况。这也是解决ABA问题目前采用的策略。
就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。

CAS总结

任何技术都不是完美的,解决的某种场景的问题。CAS也有它的缺点

  • CAS实际上是一种自旋锁,一直循环,开销比较大
  • 只能保证一个变量的原子性操作,多个变量依然要加锁
  • 引出了ABA问题(AtomicStampedReference可解决)

以上是关于并发-CAS 原理浅解01的主要内容,如果未能解决你的问题,请参考以下文章

并发01--并发存在的问题及底层实现原理

Java并发基石-CAS原理实战

Java并发基石-CAS原理实战

并发编程之 CAS 的原理

并发编程之 CAS 的原理

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