• CAS详解


    什么叫CAS?

    CAS,是 compare and swap 的缩写,即比较并交换。它是一种基于乐观锁的操作。它有三个操作数,内存值V,预期值A,更新值B。当且仅当A和V相同时,才会把V修改成B,否则什么都不做。之前说到AtomicInteger用到了CAS,那么先从这个类说起。看如下代码:

    public static void main(String[] args){
            AtomicInteger atomicInteger = new AtomicInteger(5);
            System.out.println(atomicInteger.compareAndSet(5,50));
            System.out.println(atomicInteger.compareAndSet(5,100));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    AtomicInteger有一个compareAndSet方法,有两个操作数,第一个是期望值,第二个是希望修改成的值。首先初始值是5,第一次调用compareAndSet方法的时候,将5拷贝回自己的工作空间,然后改成50,写回到主内存中的时候,它期望主内存中的值是5,而这时确实也是5,所以可以修改成功,主内存中的值也变成了50,输出true。第二次调用compareAndSet的时候,在自己的工作内存将值修改成100,写回去的时候,希望主内存中的值是5,但是此时是50,所以set失败,输出false。这就是比较并交换,也即CAS。

    CAS的工作原理

    简而言之,CAS工作原理就是UnSafe类自旋锁
    1、UnSafe类:
    UnSafe类在jdk的rt.jar下面的一个类,全包名是sun.misc.UnSafe。这个类大多数方法都是native方法。由于Java不能操作计算机系统,所以设计之初就留了一个UnSafe类。通过UnSafe类,Java就可以操作指定内存地址的数据。调用UnSafe类的CAS,JVM会帮我们实现出汇编指令,从而实现原子操作。现在就来分析一下AtomicInteger的getAndIncrement方法是怎么工作的。看下面的代码:

     public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
     }
    
    • 1
    • 2
    • 3

    这个方法调用的是unsafe类的getAndAddInt方法,有三个参数。第一个表示当前对象,也就是你new 的那个AtomicInteger对象;第二个表示内存地址;第三个表示自增步伐。然后再点进去看看这个getAndAddInt方法。

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里的val1就是当前对象,val2是内存地址,val4是1,也就是自增步伐。首先把当前对象主内存中的值赋给val5,然后进入while循环。判断当前对象此刻主内存中的值是否等于val5,如果是,就自增,否则继续循环,重新获取val5的值。这里的compareAndSwapInt方法就是一个native方法,这个方法汇编之后是CPU原语指令,原语指令是连续执行不会被打断的,所以可以保证原子性。

    2、自旋锁:
    所谓的自旋,其实就是上面getAndAddInt方法中的do while循环操作。当预期值和主内存中的值不等时,就重新获取主内存中的值,这就是自旋。

    CAS的缺点

    缺点有三个。
    1、循环时间长,开销大。
    synchronized是加锁,同一时间只能一个线程访问,并发性不好。而CAS并发性提高了,但是由于CAS存在自旋操作,即do while循环,如果CAS失败,会一直进行尝试。如果CAS长时间不成功,会给CPU带来很大的开销。

    2、只能保证一个共享变量的原子性。
    上面也看到了,getAndAddInt方法的val1是代表当前对象,所以它也就是能保证这一个共享变量的原子性。如果要保证多个,那只能加锁了。

    3、引来的ABA问题。

    • 什么是ABA问题?

    假设现在主内存中的值是A,现有t1和t2两个线程去对其进行操作。t1和t2先将A拷贝回自己的工作内存。这个时候t2线程将A改成B,刷回到主内存。此刻主内存和t2的工作内存中的值都是B。接下来还是t2线程抢到执行权,t2又把B改回A,并刷回到主内存。这时t1终于抢到执行权了,自己工作内存中的值的A,主内存也是A,因此它认为没人修改过,就在工作内存中把A改成了X,然后刷回主内存。也就是说,在t1线程执行前,t2将主内存中的值由A改成B再改回A。这便是ABA问题。看下面的代码演示(代码涉及到原子引用,请参考下面的原子引用的介绍):

    class ABADemo {
       static AtomicReference atomicReference = new AtomicReference<>("A");
       public static void main(String[] args){
              new Thread(() -> {
                  atomicReference.compareAndSet("A","B");
                  atomicReference.compareAndSet("B","A");
                  },"t2").start();
              new Thread(() -> {
                  try { 
                       TimeUnit.SECONDS.sleep(1);
                  } catch (InterruptedException e) {
                       e.printStackTrace(); 
                  }
                  System.out.println(atomicReference.compareAndSet("A","C") 
                                               + "\t" + atomicReference.get());
                  },"t1").start();
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这段代码执行结果是"true C",这就证明了ABA问题的存在。如果一个业务只管开头和结果,不管这个A中间是否变过,那么出现了ABA问题也没事。如果需要A还是最开始的那个A,中间不许别人动手脚,那么就要规避ABA问题。要解决ABA问题,先看下面的原子引用的介绍。

    • 原子引用:

    JUC包下给我们提供了原子包装类,像AtomicInteger。如果我不仅仅想要原子包装类,我自己定义的User类也想具有原子操作,怎么办呢?JUC为我们提供了AtomicReference,即原子引用。看下面的代码:

    @AllArgsConstructor
    class User {
        int age;
        String name;
    
        public static void main(String[] args){
            User user = new User(20,"张三");
            AtomicReference atomicReference = new AtomicReference<>();
            atomicReference.set(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    像这样,就把User类变成了原子User类了。

    • 解决ABA问题思路:

    我们可以这个共享变量带上一个版本号。比如现在主内存中的是A,版本号是1,然后t1和t2线程拷贝一份到自己工作内存。t2将A改为B,刷回主内存。此时主内存中的是B,版本号为2。然后再t2再改回A,此时主内存中的是A,版本号为3。这个时候t1线程终于来了,自己工作内存是A,版本号是1,主内存中是A,但是版本号为3,它就知道已经有人动过手脚了。那么这个版本号从何而来,这就要说说AtomicStampedReference这个类了。

    • 带时间戳的原子引用(AtomicStampedReference):
      这个时间戳就理解为版本号就行了。看如下代码:
    class ABADemo {
            static AtomicStampedReference atomicReference = new AtomicStampedReference<>("A", 1);
            public static void main(String[] args) {
                new Thread(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(1);// 睡一秒,让t1线程拿到最初的版本号
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    atomicReference.compareAndSet("A", "B", atomicReference.getStamp(), atomicReference.getStamp() + 1);
                    atomicReference.compareAndSet("B", "A", atomicReference.getStamp(), atomicReference.getStamp() + 1);
                }, "t2").start();
                new Thread(() -> {
                    int stamp = atomicReference.getStamp();//拿到最开始的版本号
                    try {
                        TimeUnit.SECONDS.sleep(3);// 睡3秒,让t2线程的ABA操作执行完
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(atomicReference.compareAndSet("A", "C", stamp, stamp + 1));
                }, "t1").start();
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    初始版本号为1,t2线程每执行一次版本号加。等t1线程执行的时候,发现当前版本号不是自己一开始拿到的1了,所以set失败,输出false。这就解决了ABA问题。

    总结:

    1.什么是CAS? ------ 比较并交换,主内存值和工作内存值相同,就set为更新值。
    2.CAS原理是什么? ------ UnSafe类和自旋锁。理解那个do while循环。
    3.CAS缺点是什么? ------ 循环时间长会消耗大量CPU资源;只能保证一个共享变量的原子性操作;造成ABA问题。
    4.什么是ABA问题? ------ t2线程先将A改成B,再改回A,此时t1线程以为没人修改过。
    5.如何解决ABA问题?------ 使用带时间戳的原子引用。

  • 相关阅读:
    双靶向融合蛋白标记的红细胞膜包裹PLGA微球/细胞膜拮抗联合纳米酶的仿生制备
    千变万化的Promise
    elasticsearch源码解析TODO列表
    java中转义字符的源码数据格式,内存存储数据格式和转换json后的数据格式
    3.Mybatis 注解方式的基本用法
    【Java8新特性】Optional类在处理空值判断场景的应用 回避空指针异常 编写健壮的应用程序
    Java 新手入门:基础知识点一览
    增删改查模块测试用例设计
    使用.NET开发VSTO工具快速将PPT导出为图片
    区块链备案
  • 原文地址:https://blog.csdn.net/weixin_64752717/article/details/132823291