• 并发编程系列-CAS


    锁(lock)的代价

    锁是用来做并发最简单的方式,其代价也是最高的,Java在JDK1.5之前都是靠synchronized关键字来加锁。但是加锁机制会有如下几个问题:

    • 加锁、释放锁会需要操作系统进行上下文切换和调度延时,在上下文切换的时候,cpu之前缓存的指令和数据都将失效,这个过程将增加系统开销。

    • 多个线程同时竞争锁,锁竞争机制本身需要消耗系统资源。没有获取到锁的线程会被挂起直至获取锁,在线程被挂起和恢复执行的过程中也存在很大开销。

    • 等待锁的线程会阻塞,影响实际的使用体验。如果被阻塞的线程优先级高,而持有锁的线程优先级低,将会导致优先级反转(Priority Inversion)。

    乐观锁与悲观锁

    悲观锁:是认为别的线程会修改值。 独占锁是一种悲观锁,synchronized就是一种独占锁。synchronized加锁后就能够确保程序执行时不会被其它线程干扰,得到正确的结果。

    乐观锁:本质上是乐观的,认为别的线程不会去修改值。如果发现值被修改了,可以再次重试。CAS机制(Compare And Swap)就是一种乐观锁。

    Compare And Swap

    CAS是一种有名的无锁(lock-free)算法。也是一种现代 CPU 广泛支持的CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。

    CAS有三个操作参数:

    1. 内存位置V(它的值是我们想要去更新的)

    2. 预期原值A(上一次从内存中读取的值)

    3. 新值B(应该写入的新值)

    CAS的操作过程:将内存位置V的值与A比较(compare),如果相等,则说明没有其它线程来修改过这个值,所以把内存V的的值更新成B(swap),如果不相等,说明V上的值被修改过了,不更新,而是返回当前V的值,再重新执行一次任务再继续这个过程。

    所以,当多个线程尝试使用CAS同时更新同一个变量时,其中一个线程会成功更新变量的值,剩下的会失败。失败的线程可以重试或者什么也不做。

    简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我这个值现在是多少”。(这段描述引自《Java并发编程实践》)

    JVM对CAS的支持

    在JDK1.5之前,如果不编写明确的代码就无法执行CAS操作,在JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操作,并且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,如果处理器/CPU不支持CAS指令,那么JVM将使用自旋锁。

    在原子类变量中,如java.util.concurrent.atomic包下的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。

    java.util.concurrent.atomic.AtomicLong源码中的自增getAndIncrement()方法:

        //+1操作
        public final long getAndIncrement() {
            while (true) {
                long current = get();
                long next = current + 1;
                //当+1操作成功的时候直接返回,退出此循环
                if (compareAndSet(current, next))
                    return current;
            }
        }


        //调用JNI实现CAS
        public final boolean compareAndSet(long expect, long update) {
            return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
        }
    • 1

    JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。在jdk1.8后getAndIncrement()方法已经看不到具体代码了,而是封装在unsafe类里面。

    CAS优缺点

    缺点

    CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。

    1. ABA问题。

      因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

      从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    2. 循环时间长开销大。

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

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

    4. 比较花费CPU资源,即使没有任何争用也会做一些无用功。

    5. 会增加程序测试的复杂度,稍不注意就会出现问题。

    优点

    • 可以保证变量操作的原子性;

    • 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;

    • 在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。

    Java提供的CAS操作类--Unsafe类

    从Java5开始引入了对CAS机制的底层的支持,在这之前需要开发人员编写相关的代码才可以实现CAS。在原子变量类Atomic**_中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代码,在这里的代码都是调用了底层(核心代码调用native修饰的方法)的实现方法。在AtomicInteger源码中可以看getAndSet方法和compareAndSet方法之间的关系,compareAndSet方法调用了底层的实现,该方法可以实现与一个volatile变量的读取和写入相同的效果。在前面说到了volatile不支持例如i++这样的复合操作,在Atomic*中提供了实现该操作的方法。JVM对CAS的支持通过这些原子类(Atomic_)暴露出来,供我们使用。

    而Atomic系类的类底层调用的是Unsafe类的API,Unsafe类提供了一系列的compareAndSwap*方法,下面就简单介绍下Unsafe类的API:

    • long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe类获取变量value在AtomicLong对象中的内存偏移。
    static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
    }
    • 1
    • int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。

    • int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。

    • boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。

    • public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。

    • void putLongvolatile(Object obj, long offset, long value)方法:设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。

    • void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。

    • void park(boolean isAbsolute, long time)方法:阻塞当前线程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。

    • void unpark(Object thread)方法:唤醒调用park后阻塞的线程。

    下面是JDK8新增的函数,这里只列出Long类型操作。

    • long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
    //这个方法只是封装了compareAndSwapLong的使用,不需要自己写重试机制
    public final long getAndSetLong(Object var1, long var2, long var4) {
     long var6;
     do {
         var6 = this.getLongVolatile(var1, var2);
     } while(!this.compareAndSwapLong(var1, var2, var6, var4));

     return var6;
    }
    • 1
    • long getAndAddLong(Object obj, long offset, long addValue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addValue,原理和上面的方法类似。

    总结

    可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题。

    顶尖架构师栈

    关注回复关键字

    【C01】超10G后端学习面试资源

    【IDEA】最新IDEA激活工具和码及教程

    【JetBrains软件名】 最新软件激活工具和码及教程

    工具&码&教程

    转载于:https://mp.weixin.qq.com/s/ELc_9k3QwEHdse_yFatDPw

    本文由 mdnice 多平台发布

  • 相关阅读:
    3.0 设计模式汇总
    Java新手小白入门篇 API -Socket网络编程
    时间序列预测 Graph-WaveNet:Graph WaveNet for Deep Spatial-Temporal Graph Modeling
    HTML期末作业:基于html+css+javascript+jquery实现古诗词网页 学生网页设计作品 web前端开发技术 web课程设计 网页规划与设计
    两数之和 三数之和【基础算法精讲 01】
    软件测试 - 基础理论篇
    深入理解计算机网络-8网络层2
    CAD圆锥齿轮画法
    LangChain 开发LLM的框架
    Spring Boot+Vue3前后端分离实战wiki知识库系统之Vue3 + Vue CLI 项目搭建
  • 原文地址:https://blog.csdn.net/Dc253061007/article/details/133161899