CAS 原理

一、CAS 是什么

Compara And Swap 比较并交换
  public void compareAndSwap(int v,int a,int b) {
        if (v == a) {
            v = b;
        }else {
            // 什么也不做
        }
    }

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。

如果内存位置的值 V 与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B 。
否则,处理器不做任何操作。

CAS操作可以防止内存中共享变量出现脏读脏写问题,多核的CPU在多线程的情况下经常出现的问题,通常我们采用锁来避免这个问题,但是CAS操作避免了多线程的竞争锁,上下文切换和进程调度。是非阻塞的乐观锁。

乐观锁:
如名一样,每次操作都认为不会发生冲突,尝试执行,并检测结果是否正确。如果正确则执行成功,否则说明发生了冲突,回退再重新尝试。
乐观锁的过程可以分为两步:冲突检测 和 数据更新。在Java多线程中乐观锁一个常见实现即:CAS操作。

二、CAS 的实现原理

Java 中的 CAS 操作都是通过调用 Unsafe的 native 方法来实现的。具体有:

compareAndSwapInt()和compareAndSwapLong(),compareAndSwapObject几个方法实现

这里可以先看下compareAndSwapInt方法

public final boolean compareAndSwapInt( Object o,    long offset,
                                       int expected, int x) {
        return theInternalUnsafe.compareAndSetInt(o, offset, expected, x);
    }

compareAndSwapInt有4个参数,第一个参数object是当前对象,第二个参数offest表示该变量在内存中的偏移地址(CAS底层是根据内存偏移位置来获取的),第三个参数expected为旧值,第四个参数x为新值。

最终调用的汇编代码片段:

```c++
// 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:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest,
jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}

```

LOCK_IF_MP(mp)会根据mp的值来决定是否为cmpxchg指令添加lock前缀。
如果通过mp判断当前系统是多处理器(即mp值为1),则为cmpxchg指令添加lock前缀。
否则,不加lock前缀。

这是一种优化手段,认为单处理器的环境没有必要添加lock前缀,只有在多核情况下才会添加lock前缀,因为lock会导致性能下降。cmpxchg是汇编指令,作用是比较并交换操作数。

使用CPU的锁机制,确保了整个CAS操作的原子性。

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

  1. 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
  2. 禁止该指令与之前和之后的读和写指令重排序。
  3. 把写缓冲区中的所有数据刷新到内存中。

上面的第1点保证了CAS操作是一个原子操作,
第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。

三、Java 中 CAS的缺点:

1.CPU开销较大

我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3.ABA 问题

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

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

四、concurrent包中CAS的应用

Java的CAS操作可以实现现代CPU上硬件级别的原子指令(不是依靠JVM或者操作系统的锁机制),而同时volatile关键字又保证了线程间共享变量的可见性和指令的顺序性,因此凭借这两种手段,就可以实现不依靠操作系统实现的锁机制来保证并发时共享变量的一致性。

如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

首先,声明共享变量为volatile;
然后,使用CAS的原子条件更新来实现线程之间的同步;
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。