CAS原理分析及ABA问题详解

image

什么是CAS

CASCompare And Swap的缩写,翻译成中文就是比较并交换,其做用是让CPU比较内存中某个值是否和预期的值相同,若是相同则将这个值更新为新值,不相同则不作更新,也就是CAS是原子性的操做(读和写二者同时具备原子性),其实现方式是经过借助C/C++调用CPU指令完成的,因此效率很高。
CAS的原理很简单,这里使用一段Java代码来描述java

public boolean compareAndSwap(int value, int expect, int update) {
//        若是内存中的值value和指望值expect同样 则将值更新为新值update
    if (value == expect) {
        value = update;
        return true;
    } else {
        return false;
    }
}
复制代码

大体过程是将内存中的值、咱们的指望值、新值交给CPU进行运算,若是内存中的值和咱们的指望值相同则将值更新为新值,不然不作任何操做。这个过程是在CPU中完成的,这里很差描述CPU的工做过程,就拿Java代码来描述了。segmentfault

Unsafe源码分析

Java是在Unsafe(sun.misc.Unsafe)类实现CAS的操做,而咱们知道Java是没法直接访问操做系统底层的API的(缘由是Java的跨平台性限制了Java不能和操做系统耦合),因此Java并无在Unsafe类直接实现CAS的操做,而是经过**JDI(Java Native Interface)**本地调用C/C++语言来实现CAS操做的。
Unsafe有不少个CAS操做的相关方法,这里举例几个安全

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
复制代码

咱们拿public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);进行分析,这个方法是比较内存中的一个值(整型)和咱们的指望值(var4)是否同样,若是同样则将内存中的这个值更新为var5,参数中的var1是值所在的对象,var2是值在对象(var1)中的内存偏移量,参数var1和参数var2是为了定位出值所在内存的地址
bash

Unsafe.java在这里发挥的做用有:

  1. 将对象引用、值在对象中的偏移量、指望的值和欲更新的新值传递给Unsafe.cpp
  2. 若是值更新成功则返回true给开发者,没有更新则返回false

Unsafe.cpp在这里发挥的做用有:多线程

  1. 接受从Unsafe传递过来的对象引用、偏移量、指望的值和欲更新的新值,根据对象引用和偏移量计算出值的地址,而后将值的地址、指望的值、欲更新的新值传递给CPU
  2. 若是值更新成功则返回trueUnsafe.java,没有更新则返回false

CPU在这里发挥的做用:源码分析

  1. 接受从Unsafe.cpp传递过来的地址、指望的值和欲更新的新值,执行指令cmpxchg,比较地址中的值是否和指望的值同样,同样则将值更新为新的值,不同则不作任何操做
  2. 将操做结果返回给Unsafe.cpp

CAS的缺点

CAS虽然高效的实现了原子性操做,可是也存在一些缺点,主要表如今如下三个方面。ui

ABA问题

在多线程场景下CAS会出现ABA问题,关于ABA问题这里简单科普下,例若有2个线程同时对同一个值(初始值为A)进行CAS操做,这三个线程以下spa

  1. 线程1,指望值为A,欲更新的值为B
  2. 线程2,指望值为A,欲更新的值为B

线程1抢先得到CPU时间片,而线程2由于其余缘由阻塞了,线程1取值与指望的A值比较,发现相等而后将值更新为B,而后这个时候出现了线程3,指望值为B,欲更新的值为A,线程3取值与指望的值B比较,发现相等则将值更新为A,此时线程2从阻塞中恢复,而且得到了CPU时间片,这时候线程2取值与指望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操做,可是线程2并不知道值已经通过了A->B->A的变化过程。操作系统

ABA问题带来的危害
小明在提款机,提取了50元,由于提款机问题,有两个线程,同时把余额从100变为50
线程1(提款机):获取当前值100,指望更新为50,
线程2(提款机):获取当前值100,指望更新为50,
线程1成功执行,线程2某种缘由block了,这时,某人给小明汇款50
线程3(默认):获取当前值50,指望更新为100,
这时候线程3成功执行,余额变为100,
线程2从Block中恢复,获取到的也是100,compare以后,继续更新余额为50!!!
此时能够看到,实际余额应该为100(100-50+50),可是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。线程

解决方法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A

循环时间长开销大

若是CAS操做失败,就须要循环进行CAS操做(循环同时将指望值更新为最新的),若是长时间都不成功的话,那么会形成CPU极大的开销。

这种循环也称为自旋

解决方法: 限制自旋次数,防止进入死循环。

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

CAS的原子操做只能针对一个共享变量。

解决方法: 若是须要对多个共享变量进行操做,可使用加锁方式(悲观锁)保证原子性,或者能够把多个共享变量合并成一个共享变量进行CAS操做。

CAS的应用

咱们知道CAS操做并不会锁住共享变量,也就是一种非阻塞的同步机制,CAS就是乐观锁的实现。

  1. 乐观锁 乐观锁老是假设最好的状况,每次去操做数据都认为不会被别的线程修改数据,因此在每次操做数据的时候都不会给数据加锁,即在线程对数据进行操做的时候,别的线程不会阻塞仍然能够对数据进行操做,只有在须要更新数据的时候才会去判断数据是否被别的线程修改过,若是数据被修改过则会拒绝操做而且返回错误信息给用户。
  2. 悲观锁 悲观锁老是假设最坏的状况,每次去操做数据时候都认为会被的线程修改数据,因此在每次操做数据的时候都会给数据加锁,让别的线程没法操做这个数据,别的线程会一直阻塞直到获取到这个数据的锁。这样的话就会影响效率,好比当有个线程发生一个很耗时的操做的时候,别的线程只是想获取这个数据的值而已都要等待好久。

Java利用CAS的乐观锁、原子性的特性高效解决了多线程的安全性问题,例如JDK1.8中的集合类ConcurrentHashMap、关键字volatileReentrantLock等。

参考

JAVA CAS原理深度分析
Java CAS 原理分析
什么是ABA问题?

原文地址:ddnd.cn/2019/03/13/…

相关文章
相关标签/搜索