【CAS之1】CAS操做原理

参考网页

主要参考html

https://www.jianshu.com/p/fb6e91b013ccjava

http://zl198751.iteye.com/blog/1848575算法

https://www.xilidou.com/2018/02/01/java-cas/缓存

其余参考安全

https://my.oschina.net/huangcongmin12/blog/692907多线程

 

http://www.cnblogs.com/dayhand/p/3713303.html并发

https://my.oschina.net/vshcxl/blog/795495app

http://www.javashuo.com/article/p-ckzisqmq-mv.htmljvm

https://mritd.me/2017/02/06/java-cas/函数

CAS是什么

CAS: 全称Compare and swap,字面意思:“比较并交换”,一个 CAS 涉及到如下操做:

假设内存中的原数据V,旧的预期值A,须要修改的新值B

  1. 比较 A 与 V 是否相等。(比较)
  2. 若是比较相等,将 B 写入 V。(交换)
  3. 返回操做是否成功。

当多个线程同时对某个资源进行CAS操做,只能有一个线程操做成功,可是并不会阻塞其余线程,其余线程只会收到操做失败的信号。可见 CAS 实际上是一个乐观锁。

为什么说当多个线程同时对某个资源进行CAS操做,只能有一个线程操做成功?多个线程访问,只有一次能成功的进行CAS操做(由于只有一个线程luck thread能够成功的捕捉到A和V相等的那一刻,而后其余线程访问时A和V确定已经不相等了)。

CAS算法原理的一个描述

CAS 算法大体原理是:在对变量进行计算以前(如 ++ 操做),首先读取原变量值,称为 旧的预期值 A,而后在更新以前再获取当前内存中的值,称为 当前内存值 V,若是 A==V 则说明变量从未被其余线程修改过,此时将会写入新值 B,若是 A!=V 则说明变量已经被其余线程修改过,当前线程应当什么也不作。

CAS和synchronized

CAS和synchronized的区别

CAS是乐观锁,synchronized(重量级锁)是悲观锁。

Java技术发展史先设计出的synchronized,synchronized会产生阻塞问题,后来又发展出了CAS。

阅读JDK的源码可知,J.U.C包其实是创建在CAS操做基础上的。ReentrantLock这些类的底层其实就采用的CAS操做。

CAS和synchronized没有绝对的好坏,各有各自适用的场景

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

不想引入锁或者想非阻塞的完成并发操做能够考虑使用CAS操做。可是CAS操做的代码可读性差难测试,复杂操做最好不用引入CAS

★CAS实现原理:硬件层级的原子操做--AtomInteger为例进行分析

AtomicInteger -> sum.misc.Unsafe

AtomicInteger核心代码

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;


    static {
        try {
            valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }


    private volatile int value;
    public final int get() {
        return value;
    }
}

代码分析

1.Unsafe,是CAS的核心类,因为Java方法没法直接访问底层系统,须要经过本地(native)方法来访问,Unsafe至关于一个后门,基于该类能够直接操做特定内存的数据。

2.变量valueOffset,表示该变量值在内存中的偏移地址,由于Unsafe就是根据内存偏移地址获取数据的。

3.变量value用volatile修饰,保证了多线程之间的内存可见性。

AtomicInteger如何实现并发下的累加操做

public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

上述代码已经使用到了Unsafe的方法。

sum.misc.Unsafe -> native(JNI方法)

unsafe.getAndAddInt

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;
}

代码分析

假设线程A和线程B同时执行getAndAdd操做(分别跑在不一样CPU上):

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
  2. 线程A经过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也经过getIntVolatile(var1, var2)方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为2。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现本身手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提早修改过了,那只能从新来一遍了。
  5. 从新获取value值,由于变量value被volatile修饰,因此其它线程对它的修改,线程A老是可以看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

整个过程当中,利用CAS保证了对于value的修改的并发安全,继续深刻看看Unsafe类中的compareAndSwapInt方法实现。

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。

native(JNI方法)-> unsafe.cpp

unsafe.cpp

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

代码分析

先想办法拿到变量value在内存中的地址。

经过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。

unsafe.cpp -> Atomic::cmpxchg

若是是Linux的x86,Atomic::cmpxchg方法的实现以下

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::is_MP();
    __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                        : "=a" (exchange_value)
                        : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                        : "cc", "memory");
    return exchange_value;
}

代码分析

__asm__表示汇编的开始
volatile表示禁止编译器优化
LOCK_IF_MP是个内联函数

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

Window的x86实现以下

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判断是不是多处理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
                       __asm je L0 \
                       __asm _emit 0xF0 \
                       __asm L0:

代码分析

LOCK_IF_MP根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。

1.若是是多处理器,为cmpxchg指令添加lock前缀。

2.反之,就省略lock前缀。(单处理器会不须要lock前缀提供的内存屏障效果)

Atomic::cmpxchg -> lock前缀

intel手册对lock前缀的说明以下:

1.确保后续指令执行的原子性。

在Pentium及以前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时没法经过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大下降lock前缀指令的执行开销。

2.禁止该指令与前面和后面的读写指令重排序。

3.把写缓冲区的全部数据刷新到内存中。

上面的第2点和第3点所具备的内存屏障效果,保证了CAS同时具备volatile读和volatile写的内存语义。

总结:AtomicInteger -> sum.misc.Unsafe -> native(JNI方法)-> unsafe.cpp -> Atomic::cmpxchg -> lock前缀

★总结一下 JAVA 的 cas 是怎么实现的

    java 的 cas 利用的的是 unsafe 这个类提供的 cas 操做。

    unsafe 的cas 依赖了的是 jvm 针对不一样的操做系统实现的 Atomic::cmpxchg

    Atomic::cmpxchg 的实现使用了汇编的 cas 操做,并使用 cpu 硬件提供的 lock信号保证其原子性

CAS的缺点

  1. ABA
  2. 循环时间长开销大
  3. 只能保证一个共享变量的原子操做

JDK9 改变

相关文章
相关标签/搜索