【CAS之1】CAS操作原理

参考网页

主要参考

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

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/795495

https://blog.csdn.net/chenssy/article/details/69640293

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 改变

转载于:https://my.oschina.net/u/3866531/blog/2052359

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值