• Java实现CAS的原理


    前言

    在并发编程中我们都知道 i++ 操作是非线程安全的,这是因为 i++ 操作不是原子操作。
    如何保证原子性呢?常用的方法就是加锁。 在Java语言中可以使用 synchronizedCAS 实现加锁效果。
    synchronized 是悲观锁,线程开始执行的第一步就是获取锁,一旦获取锁,其他线程进入后就会阻塞等待锁,而后在争夺到锁资源后恢复为RunNable状态,这个过程涉及到操作系统用户模式和内核模式的转换,代价比较高。
    CAS 是乐观锁,线程执行的时候不会加锁,假设没有冲突去完成某项操作,如果因为冲突失败了就重试,最后直到成功为止。

    CAS的概念

    CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:

    V:要更新的变量(var)
    E:预期值(expected)
    N:新值(new) 比较并交换的过程如下:

    判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

    所以这里的预期值E本质上指的是“旧值”。

    举例:
    1.如果有一个多个线程共享的变量 i 原本等于 5,现在在线程A中,想把它设置为新的值6;
    2.使用CAS来做这个事情
    3.首先用 i 去与 5 对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,
    i 的值被设置成了6;
    4.如果不等于5,说明i被其它线程改过了(比如现在i的值为2),那么我就什么也不做,此次CAS失败,i的值仍
    然为2。

    在这个例子中,i就是V,5就是E,6就是N。

    那有没有可能我在判断了i为5之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?

    不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性

    当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

    Java实现CAS的原理-Unsafe

    CAS是一种原子操作。那么Java是怎样来使用CAS的呢?我们知道,在Java方法中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM 使用c或者c++去实现。

    在Java中,有一个Unsafe类,它在 sun.misc 包中,它里面是一些 native 方法,其中就有几个关于 CAS 的:

    boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
    boolean compareAndSwapInt(Object o, long offset,int expected,int x);
    boolean compareAndSwapLong(Object o, long offset,long expected,long x);
    
    
    • 1
    • 2
    • 3
    • 4

    当然,他们都是 public native的。
    Unsafe中对 CAS 的实现是C++ 写的,它的具体实现和操作系统、CPU都有关系

    Linux的X86下主要通过 cmpxchgl 这个指令在CPU 级完成CAS操作的,但在多处理器情况下必须使用lock指令加锁来完成。

    Unsafe类里面还有其他方法用于不同的用途。比如支持线程挂起和恢复的 parkunpark, LockSupper类底层就是调用了这两个方法。还有支持反射操作的 allocateInstance()方法。

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

    循环时间长,CPU开销过大

    CAS 多与自旋结合,如果自旋CAS 长时间不成功,会占用大量的 CPU资源。
    例:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

    解决思路是让JVM 支持处理器提供的 pause 指令
    pause 指令能让自旋失败时cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,未解决内存顺序冲突而导致 的CPU 流水线重排 的代价也会小很多。

    延伸–自旋锁和互斥锁

    有两个线程,线程A去获得一个临界区的锁,而这个锁正被线程B锁持有
    互斥锁:线程A会被阻塞
    自旋锁:一直循环等待,进行锁请求,直到获得这个锁为止。

    自旋锁的作用是为了解决某项资源的互斥使用,因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁
    不足之处:自旋锁一直占用着CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短时间内获得锁,会使CPU效率降低

    详情参考至一缕阳光a的博客

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

    解决方案:
    1.使用JDK1.5开始就提供的AtomicReference 类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作;
    2.使用锁。锁内的临界区代码可以保证只有当前线程能操作。

    ABA问题

    例:
    假设小明要到银行取钱,此时账户余额为100元,小明取款50,由于硬件出问题,取款操作被提交了两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元,理想情况下应该是一个线程更新成功一个失败,小明的存款值被扣一次。

    线程1:获取当前值100,期望更新为 50
    线程2:获取当前值100,期望更新为50

    线程1 首先执行成功,把余额修改为50元,线程2 因为某种原因阻塞。这时,小明的妈妈刚好给小明汇款50元

    线程1:获取当前值100,成功更新为50
    线程2:获取当前值100,希望更新为50,BLOCK
    线程3:获取当前值50,希望更新为 100

    线程2 仍然是阻塞状态,线程3执行成功,把余额从50改为100

    线程1:获取当前值100,成功更新为50,已返回
    线程2:获取当前值100,希望更新为50,BLOCK
    线程3:获取当前值50,成功更新为 100

    线程2恢复运行,由于阻塞之前获得了“当前值“100,并且经过 compare检测,此时存款值也是100,所以会成功把变量值100更新为50

    线程1:获取当前值100,成功更新为50,已返回
    线程2:获取当前值100,希望更新为50,成功更新为50
    线程3:获取当前值50,成功更新为 100,已返回

    原本线程2 应当提交失败,小明的正确余额应该为100元,结果由于ABA问题提交成功了。
    解决方法:
    加版本号或者时间戳
    从JDK1.5开始,JDK的atomic包里提供了一个类 AtomicStampedRefernce 类来解决ABA 问题
    这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用CAS设置为新的值和标志。

    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)));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    智能供应链管理系统数字化供应链全周期管理,提升企业市场竞争力
    EtherCAT从站EEPROM组成信息详解(2):字8-15产品标识区
    vue3子页面刷新,v-show联合v-if实现柱状图隐藏展示,和按条件查询
    Linux 入门:基本指令
    大一新生HTML期末作业,网页制作作业——明星介绍易烊千玺网站HTML+CSS
    vue项目使用electron打包exe桌面程序
    正则表达式re总结
    python 笔记
    嵌入式期末复习
    计算机竞赛 题目:基于深度学习的图像风格迁移 - [ 卷积神经网络 机器视觉 ]
  • 原文地址:https://blog.csdn.net/qq_45718545/article/details/127620335