zoukankan      html  css  js  c++  java
  • 高并发之CAS机制和ABA问题

    什么是CAS机制

    CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换

    CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

    看如下几个例子:

    package com.example.demo.concurrentDemo;
    
    import org.junit.Test;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class CasTest {
    
        private static int count = 0;
    
        @Test
        public void test1(){
            for (int j = 0; j < 2; j++) {
                new Thread(() -> {
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                }).start();
            }
    
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            //结果必定  count <= 20000
            System.out.println(count);
        }
    
        @Test
        public void test2() {
            for (int j = 0; j < 2; j++) {
                new Thread(() -> {
                    for (int i = 0; i < 10000; i++) {
                        synchronized (this) {
                            count++;
                        }
                    }
                }).start();
            }
    
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //synchronized  类似于悲观锁
            //synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态
            //这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高
            System.out.println(count);
        }
    
        private static AtomicInteger atoCount = new AtomicInteger(0);
    
        @Test
        public void test3() {
            for (int j = 0; j < 2; j++) {
                new Thread(() -> {
                    for (int i = 0; i < 10000; i++) {
                        atoCount.incrementAndGet();
                    }
                }).start();
            }
    
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            //Atomic操作类的底层正是用到了“CAS机制”
            System.out.println(atoCount);
        }
    
    }

    CAS 缺点

    1) CPU开销过大

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

    这个可以通过看:AtomicInteger.incrementAndGet()源码,可知这是一个无限循环,获取实际值与预期值比较,当相等才会跳出循坏。

    2) 不能保证代码块的原子性

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

    3) ABA问题

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

    什么是ABA?先看下面例子:

    我们先来看一个多线程的运行场景:
    时间点1 :线程1查询值是否为A 
    时间点2 :线程2查询值是否为A 
    时间点3 :线程2比较并更新值为B 
    时间点4 :线程2查询值是否为B 
    时间点5 :线程2比较并更新值为A 
    时间点6 :线程1比较并更新值为C

    在这个线程执行场景中,2个线程交替执行。线程1在时间点6的时候依然能够正常的进行CAS操作,尽管在时间点2到时间点6期间已经发生一些意想不到的变化, 但是线程1对这些变化却一无所知,因为对线程1来说A的确还在。通常将这类现象称为ABA问题。
    ABA发生了,但线程不知道。又或者链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

    ABA隐患

    就像兵法讲的:偷梁换柱、李代桃僵 

    历史事件:赵氏孤儿

     解决ABA问题两种方法:

    1、悲观锁思路,加锁;

    2、乐观锁思路,通过AtomicStampedReference.class 

    源码实现,具体看源码:

    1. 创建一个Pair类来记录对象引用和时间戳信息,采用int作为时间戳,实际使用的时候时间戳信息要做成自增的,否则时间戳如果重复,还会出现ABA的问题。这个Pair对象是不可变对象,所有的属性都是final的, of方法每次返回一个新的不可变对象。

    2. 使用一个volatile类型的引用指向当前的Pair对象,一旦volatile引用发生变化,变化对所有线程可见。

    3. set方法时,当要设置的对象和当前Pair对象不一样时,新建一个不可变的Pair对象。

    4. compareAndSet方法中,只有期望对象的引用和版本号和目标对象的引用和版本好都一样时,才会新建一个Pair对象,然后用新建的Pair对象和原理的Pair对象做CAS操作。

    5. 实际的CAS操作比较的是当前的pair对象和新建的pair对象,pair对象封装了引用和时间戳信息。

    Demo:

     @Test
        public void test4() {
            final int timeStamp = atoReferenceCount.getStamp();
    
            new Thread(() -> {
                while(true){
                    if(atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                            atoReferenceCount.getReference()+1, timeStamp, timeStamp + 1)){
                        System.out.println("11111111");
                        break;
                    }
                }
            },"线程1:").start();
    
            new Thread(() -> {
                while(true){
                    if(atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                            atoReferenceCount.getReference()+1, timeStamp, timeStamp + 1)){
                        System.out.println("2222222");
                        break;
                    }
                }
            },"线程2:").start();
    
    
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(atoReferenceCount.getReference());
        }

    第二个没有执行,因为时间戳不对了。

    修改下代码:

     @Test
        public void test4() {
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    boolean f = atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                            atoReferenceCount.getReference() + 1, atoReferenceCount.getStamp(),
                            atoReferenceCount.getStamp() + 1);
    
                    System.out.println("线程"+Thread.currentThread()+"result="+f);
                }, "线程:"+i).start();
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(atoReferenceCount.getReference());
        }

    结果:可见线程:0,比较的时候发现时间戳变了,所以没有+1。

    demo2:

    @Test
        public void test5() {
            for (int i = 0; i < 4; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 500; j++) {
                        boolean f = atoReferenceCount.compareAndSet(atoReferenceCount.getReference(),
                                atoReferenceCount.getReference() + 1, atoReferenceCount.getStamp(),
                                atoReferenceCount.getStamp() + 1);
    
                        System.out.println("线程"+Thread.currentThread()+">>j="+j+",result="+f);
                    }
                }, "线程:"+i).start();
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(atoReferenceCount.getReference());
        }

     有3次比较时间戳发现已经不同

    参考:

    https://blog.csdn.net/qq_32998153/article/details/79529704

  • 相关阅读:
    【BZOJ】2019: [Usaco2009 Nov]找工作(spfa)
    【BZOJ】3668: [Noi2014]起床困难综合症(暴力)
    Redis 字符串结构和常用命令
    Redis实现存取数据+数据存取
    Spring 使用RedisTemplate操作Redis
    使用 java替换web项目的web.xml
    SQL server 从创建数据库到查询数据的简单操作
    SQL server 安装教程
    IntelliJ IDEA 注册码(因为之前的地址被封杀了,所以换了个地址)
    对程序员有帮助的几个非技术实用链接(点我,你不会后悔的)
  • 原文地址:https://www.cnblogs.com/xiaozhuanfeng/p/10846180.html
Copyright © 2011-2022 走看看