概述linux
CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时经常使用到的技术,Java并发包中的不少类都使用了CAS技术。CAS也是如今面试常常问的问题,本文将深刻的介绍CAS的原理。面试
介绍CAS以前,咱们先来看一个例子。算法
上面这个例子在volatile关键字详解文中用过,咱们知道,运行完这段代码以后,并不会得到指望的结果,并且会发现每次运行程序,输出的结果都不同,都是一个小于200000的数字。windows
经过分析字节码咱们知道,这是由于volatile只能保证可见性,没法保证原子性,而自增操做并非一个原子操做(以下图所示),在并发的状况下,putstatic指令可能把较小的race值同步回主内存之中,致使咱们每次都没法得到想要的结果。那么,应该怎么解决这个问题了?缓存
解决方法:并发
首先咱们想到的是用synchronized来修饰increase方法。函数
使用synchronized修饰后,increase方法变成了一个原子操做,所以是确定能获得正确的结果。可是,咱们知道,每次自增都进行加锁,性能可能会稍微差了点,有更好的方案吗?源码分析
答案固然是有的,这个时候咱们可使用Java并发包原子操做类(Atomic开头),例如如下代码。性能
咱们将例子中的代码稍作修改:race改为使用AtomicInteger定义,“race++”改为使用“race.getAndIncrement()”,AtomicInteger.getAndIncrement()是原子操做,所以咱们能够确保每次均可以得到正确的结果,而且在性能上有不错的提高(针对本例子,在JDK1.8.0_151下运行)。优化
经过方法调用,咱们能够发现,getAndIncrement方法调用getAndAddInt方法,最后调用的是compareAndSwapInt方法,即本文的主角CAS,接下来咱们开始介绍CAS。
getAndAddInt方法解析:拿到内存位置的最新值v,使用CAS尝试修将内存位置的值修改成目标值v+delta,若是修改失败,则获取该内存位置的新值v,而后继续尝试,直至修改为功。
CAS是什么?
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS须要有3个操做数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改成B,不然就什么都不作。整个比较并替换的操做是一个原子操做。
源码分析
上面源码分析时,提到最后调用了compareAndSwapInt方法,接着继续深刻探讨该方法,该方法在Unsafe中对应的源码以下。
能够看到调用了“Atomic::cmpxchg”方法,“Atomic::cmpxchg”方法在linux_x86和windows_x86的实现以下。
linux_x86的实现:
windows_x86的实现:
Atomic::cmpxchg方法解析:
mp是“os::is_MP()”的返回结果,“os::is_MP()”是一个内联函数,用来判断当前系统是否为多处理器。
若是当前系统是多处理器,该函数返回1。
不然,返回0。
LOCK_IF_MP(mp)会根据mp的值来决定是否为cmpxchg指令添加lock前缀。
若是经过mp判断当前系统是多处理器(即mp值为1),则为cmpxchg指令添加lock前缀。
不然,不加lock前缀。
这是一种优化手段,认为单处理器的环境没有必要添加lock前缀,只有在多核状况下才会添加lock前缀,由于lock会致使性能降低。cmpxchg是汇编指令,做用是比较并交换操做数。
intel手册对lock前缀的说明以下:
确保对内存的读-改-写操做原子执行。在Pentium及Pentium以前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其余处理器暂时没法经过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上作了一个颇有意义的优化:若是要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),而且该内存区域被彻底包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。因为在指令执行期间该缓存行会一直被锁定,其它处理器没法读/写该指令要访问的内存区域,所以能保证指令执行的原子性。这个操做过程叫作缓存锁定(cache locking),缓存锁定将大大下降lock前缀指令的执行开销,可是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
禁止该指令与以前和以后的读和写指令重排序。
把写缓冲区中的全部数据刷新到内存中。
上面的第1点保证了CAS操做是一个原子操做,第2点和第3点所具备的内存屏障效果,保证了CAS同时具备volatile读和volatile写的内存语义。
CAS的缺点:
CAS虽然很高效的解决了原子操做问题,可是CAS仍然存在三大问题。
循环时间长开销很大。
只能保证一个共享变量的原子操做。
ABA问题。
循环时间长开销很大:
咱们能够看到getAndAddInt方法执行时,若是CAS失败,会一直进行尝试。若是CAS长时间一直不成功,可能会给CPU带来很大的开销。
只能保证一个共享变量的原子操做:
当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁来保证原子性。
什么是ABA问题?ABA问题怎么解决?
若是内存地址V初次读取的值是A,而且在准备赋值的时候检查到它的值仍然为A,那咱们就能说它的值没有被其余线程改变过了吗?
若是在这段期间它的值曾经被改为了B,后来又被改回为A,那CAS操做就会误认为它历来没有被改变过。这个漏洞称为CAS操做的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它能够经过控制变量值的版原本保证CAS的正确性。所以,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,若是须要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。