• 5.CAS原理


    目录

    1. CAS 介绍

    2 CAS应用案例1-计数器

    3 CAS实现原子操作的三大问题

    3.1 ABA问题

    3.2 循环时间长开销大

    3.3 只能保证一个共享变量的原子操作


    接下来,我们深入分析CAS以及相关的延伸问题。CAS是乐观锁的重要原理,在后面很多内容中都会用到。而且即使像synchronized这样的悲观锁,其优化的锁,例如偏向锁和轻量级锁也会用到CAS,因此为了降低后面学习的压力,我们有必要先将其解释清楚。

    接下来三篇依次分析如下三个问题:

    • 1.CAS原理

    • 2.UnSafe类的功能

    • 3.原子操作的原理

    1. CAS 介绍

    先看个例子,下面这个代码如果不加锁就不能保证原子性,该问题我们在前面分析互斥失效的《原子性》做过分析,此时如果加了锁效率就会降低,该如何解决呢?

    1. public synchronized void incrementCount() {
    2.   count++;
    3. }

    这里出现错误的原因是多线程环境下执行i累加时,i的值与之前读取的值不一样了。这时候是否可以执行I累加的时候先判断一下i是否与之前读取的i的预期值相等,如果相等,则没有被修改过,可以直接累加。如果不相等 ,则表示被修改过,就不能累加呢?举一个生活中的场景就是,取钱的时候有急事走了,回来之后不知道是否被人偷了。我们可以这么做:登陆之后先看总额,比如10W,当你再来的时候先看一下总额,如果还是10W,那就没事,否则就说明被人动了手脚。这就是CAS的原理。

    此时例子就大致如下了:

    1. public void incrementCount(int expre) {
    2.   if (count == expre) {
    3.       count++;
    4.   }
    5. }

    每次调用累加之前都传入一个预期值,如果i与预期值相等,i才执行累加。

    这里有个明显的问题,上面三行代码对应的指令如下:

    1. public void incrementCount();
    2.   descriptor: ()V
    3.   flags: ACC_PUBLIC
    4.   Code:
    5.     stack=3, locals=1, args_size=1
    6.         0: aload_0
    7.         1: dup
    8.         2: getfield     #2                 // Field count:I
    9.         5: iconst_1
    10.         6: iadd
    11.         7: putfield     #2                 // Field count:I
    12.       10: return

    也就说此时if判断和i++对应多条指令,是无法保证多线程安全的。但是假如我们在JDK底层,使用特殊的方式保证这里的判断作为一个整体,就可以避免该问题。这就好比一个任务交给十个菜鸟一起做可能会带来很多问题,假如让一个高手迅速完成并上线,此时副作用就会非常小。 这个就是UnSafe类的作用,具体原理我们后面逐步介绍。

    CAS在JDK中有非常广泛的应用,叫法比较多,有CompareAndSwap、CompareAndSet、CompareAndExchange等等。其核心作用是能够实现多线程环境下对一个共享变量进行修改时原子性不变。 CAS基本原理如下,CAS方法中传递三个参数,第一个参数V表示要更新的变量,第二个参数E表示期望值,第三个参数U表示更新后的值。

    更新的要求是,如果V==E,表示预期值和实际值相等,则将V修改成U并返回true,否则修改失败并返回false。

     在Java中提供的类UnSafe类就是专门完成该任务的,其中针对int类型提供的CAS方法是

    public final native boolean compareAndSwapInt(Object var1, long var2, int expect, int update);

    从方法定义中看,它有四个参数:

    • var1:表示当前的实例对象。

    • offset:表示实际变量的内存地址变量。

    • expect:表示预期值

    • update:表示要更新的值

    expect和update比较好理解,offset表示目标变量X在实例对象var1中内存地址的偏移量。简单来说就是在预期值expect要和目标变量进行比较判断是否相等,目标X的变量的值就是通过该变量从内存中获得的。

    那为什么用UnSafe类就可以呢?我们后面再详细解释,这里先明确CAS的思想即可。

    2 CAS应用案例1-计数器

    我们继续通过一个计数器的例子来看一下CAS是怎么用的。

    CAS能够在不加锁的情况下实现线程安全的自增操作,我们设置10个线程并行运行,每个都通过CAS自旋的方式对一个共享变量的数据进行自增操作,每个线程运行500次,那么最终应该能到5000。

    首先创建CasCountIncrement类,并做基础的定义:

    1. //Unsafe对象
    2. private static final Unsafe unsafe = getUnsafe();
    3. //线程的数量
    4. private static final int THREAD_COUNT = 10;
    5. //每个线程运行count++的次数
    6. private static final int EXECUTE_COUNT_EVERY_THREAD = 500;
    7. //自增的count
    8. private volatile int count = 0;
    9. //count的偏移量
    10. private static long countOffset;

    然后定义getUnsafe()方法:

    1. private static Unsafe getUnsafe() {
    2.   Unsafe unsafe = null;
    3.   try {
    4.       Field singleoneInstanceField = Unsafe.class.getDeclaredField("theUnsafe");
    5.       singleoneInstanceField.setAccessible(true);
    6.       unsafe = (Unsafe) singleoneInstanceField.get(null);
    7.   } catch (Exception e) {
    8.       e.printStackTrace();
    9.   }
    10.   return unsafe;
    11. }

    之后,在静态代码块中通过Unsafe类的objectFieldOffset()方法获取count变量的偏移量,将值赋值给countOffset变量。

    1. static {
    2.   try {
    3.       countOffset = unsafe.objectFieldOffset
    4.               (CasCountIncrement.class.getDeclaredField("count"));
    5.   } catch (NoSuchFieldException e) {
    6.       e.printStackTrace();
    7.   }
    8. }

    最后创建incrementCountByCas()方法,这里真正实现count++

    1. public void incrementCountByCas(){
    2.   //将count的值赋值给oldCount
    3.   int oldCount = 0;
    4.   do {
    5.       oldCount = count;
    6.   }while (!unsafe.compareAndSwapInt(this, countOffset, oldCount, oldCount + 1));
    7. }

    最后写一个测试方法:

    1. public static void main(String[] args) throws InterruptedException {
    2.   CasCountIncrement casCountIncrement = new CasCountIncrement();
    3.   //为了模拟并发使用了CountDownLatch
    4.   CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
    5.   //10个线程
    6.   IntStream.range(0, THREAD_COUNT).forEach((i) -> {
    7.       new Thread(()->{
    8.           //每个线程执行500count++
    9.           IntStream.range(0, EXECUTE_COUNT_EVERY_THREAD).forEach((j) -> {
    10.               casCountIncrement.incrementCountByCas();
    11.           });
    12.           latch.countDown();
    13.       }).start();
    14.   });
    15.   latch.await();
    16.   System.out.println("count的最终结果为: " + casCountIncrement.count);
    17. }

    这里用到了CountDownLatch,其功能我们在JUC部分会深入讲解,这里只要知道通过该关键字能够实现并发就可以了。

    完整代码参考讲义配套源码中的CasCountIncrement类。

    3 CAS实现原子操作的三大问题

    在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如 LinkedTransferQueue类的Xfer方法。CAS虽然很高效地解决了原子操作,但是CAS仍然存在三 大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。

    3.1 ABA问题

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

    ABA问题的解决思路就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从 Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    3.2 循环时间长开销大

    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第 一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间 取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候 因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而 提高CPU的执行效率。

    3.3 只能保证一个共享变量的原子操作

    当对一个共享变量执行操作时,我们可以使用循 环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子 性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来 操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始, JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对 象里来进行CAS操作。

  • 相关阅读:
    Node.js精进(10)——性能监控(下)
    线性dp,优化,272. 最长公共上升子序列
    预售拼购模式是什么?有什么优势?
    计算机毕业设计Java酒店互联网平台系统(源码+mysql数据库+系统+lw文档)
    世界杯观后感
    salesforce的按钮执行js代码如何链接到apex代码
    keras图片数字识别入门AI机器学习
    JSON介绍
    Java 和低延迟
    从 Linux 其它用户复制 conda 的虚拟环境
  • 原文地址:https://blog.csdn.net/xueyushenzhou/article/details/126517408