• java CAS详解(深入源码剖析)


    CAS是什么

    CAS是compare and swap的缩写,即我们所说的比较交换。该操作的作用就是保证数据一致性、操作原子性。

    cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。否则将不做任何处理。

    CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    为什么说CAS能很好的保证数据一致性,因为它是直接从硬件层面保证了原子性

    CAS是一条CPU的原子指令(cmpxchg指令),Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

    执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的。比起用synchronized重量级锁, 这里的排他时间要短很多, 所以在多线程情况下性能会比较好。

    测试案例讲解

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class TestCas {
        public static int count = 0;
        private final static int MAX_TREAD=10;
        public static AtomicInteger atomicInteger = new AtomicInteger(0);
        public static void main(String[] args) throws InterruptedException {
            /*CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。
            使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。
            当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。*/
            CountDownLatch latch = new CountDownLatch(MAX_TREAD);
    
            //匿名内部类
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        count++;
                        atomicInteger.getAndIncrement();
                    }
                    latch.countDown(); // 当前线程调用此方法,则计数减一
                }
            };
            //同时启动多个线程
            for (int i = 0; i < MAX_TREAD; i++) {
                new Thread(runnable).start();
            }
            latch.await(); // 阻塞当前线程,直到计数器的值为0
            System.out.println("理论结果:" + 1000 * MAX_TREAD);
            System.out.println("static count: " + count);
            System.out.println("AtomicInteger: " + atomicInteger.intValue());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    我们发现每次运行,atomicInteger 的结果值都是正确的,count++的结果却不对
    在这里插入图片描述

    为什么AtomicInteger类的操作能保证数据一致性呢个?进入它的源码:

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    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;
    }
    //cas方法
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到最终是调用了Unsafe类中的compareAndSwapInt,该方法就是CAS方法其中的一个。Unsafe类中总共有三个涉及CAS的方法

    /*
    @param o 包含要修改的字段的对象
    @param offset 字段在对象内的偏移量
    @param expected 期望值(旧的值)
    @param update 更新值(新的值)
    @return true 更新成功 | false 更新失败
    */
    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
    public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
    public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在执行 Unsafe 的 CAS 方法的时候,这些方法首先将内存位置的值与预期值(旧的值)比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回 true ;如果不相匹配,处理器不做任何操作,并返回 false 。

    总的来说,AtomicInteger类主要利用CAS (compare and swap) + volatile和 native方法来保证原子操作。

    通过看上述getAndAddInt方法源码,可见如果cas失败会不断循环重复尝试。这就常说的cas自旋,所谓自旋其实是重复调用compareAndSwap方法,涉及到的方法有以下几个,也是在Unsafe类中。

    • getAndAddInt
    • getAndAddLong
    • getAndSetInt
    • getAndSetLong
    • getAndSetObject

    CAS缺点

    1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

    2. 循环时间长开销大。高并发下N多线程同时去操作一个变量,会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。

    ABA问题解决方案(AtomicStampedReference)

    JDK 的提供了一个类似 AtomicStampedReference 类来解决 ABA 问题。

    AtomicStampReference 在 CAS 的基础上增加了一个 Stamp 整型 印戳(或标记),使用这个印戳可以来觉察数据是否发生变化,给数据带上了一种实效性的检验。
    AtomicStampReference 的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳( Stamp )标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳( Stamp )标志的值更新为给定的更新值。

    1. AtomicStampReference 的构造器

      /**  
      * @param initialRef初始引用  
      * @param initialStamp初始戳记  
      */
      AtomicStampedReference(V initialRef, int initialStamp)
      
      • 1
      • 2
      • 3
      • 4
      • 5
    2. AtomicStampReference的核心方法

      AtomicStampReference类中有自己的compareAndSet方法,进入源码:

      /**
      * expectedReference 引用的旧值
      * newReference 引用的新值
      * expectedStamp 旧的戳记
      * newStamp 新的戳记 
      */
      public boolean compareAndSet(V   expectedReference,
                                   V   newReference,
                                   int expectedStamp,
                                   int newStamp) {
          Pair<V> current = pair;
          return
              expectedReference == current.reference &&
              expectedStamp == current.stamp &&
              ((newReference == current.reference &&
                newStamp == current.stamp) ||
               casPair(current, Pair.of(newReference, newStamp))); //unsafe类中的cas
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

      可以看到旧值expectedReference和旧印戳expectedStamp都会进行比较,都满足才会调用Unsafe中的cas方法。

  • 相关阅读:
    【Linux网络】1分钟使用shell脚本完成DNS主从解析服务器部署(适用于centos主机)
    QTableView练习实践00
    商城项目10_JSR303常用注解、在项目中如何使用、统一处理异常、分组校验功能、自定义校验注解
    [量化投资-学习笔记007]Python+TDengine从零开始搭建量化分析平台-布林带
    【C++ 学习 ㉕】- 万字详解 unordered_map 和 unordered_set(哈希表的查找和容器的模拟实现)
    手机和模拟器的 Frida 环境配置
    [js电子榨菜]事件传递机制 event propogate
    [THUPC2022初赛]搬砖
    0动态规划中等 LeetCode467. 环绕字符串中唯一的子字符串
    SSM+工业关键设备监测运维系统 毕业设计-附源码191400
  • 原文地址:https://blog.csdn.net/qq_43331014/article/details/133048116