目录
注意:本文参考 《吊打面试官》系列-乐观锁、悲观锁
用Java原子变量的CAS方法实现一个自旋锁 - LaplaceDemon - 博客园
说一说Java的Unsafe类 - pkufork - 博客园
CAS简介
CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。
CAS 是怎么实现线程安全的?
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
举个栗子:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“帅丙”,拿到值了,我们准备修改成name=“三歪”,在修改之前我们判断一下,原来的name是不是等于“帅丙”,如果被其他线程修改就会发现name不等于“帅丙”,我们就不进行操作,如果原来的值还是帅丙,我们就把name修改为“三歪”,至此,一个流程就结束了。
有点懵?理一下停下来理一下思路。
Tip:比较+更新 整体是一个原子操作,当然这个流程还是有问题的,我下面会提到。
他是乐观锁的一种实现,就是说认为数据总是不会被更改,我是乐观的仔,每次我都觉得你不会渣我,差不多是这个意思。
CAS的可见性与重排序
AbstractQueuedSynchronizer: compareAndSetState(int expect,int update)
该方法以原子操作的方式更新state变量,本文把Java的compareAndSet()方法调用简称为 CAS。JDK文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态 设置为给定的更新值。此操作具有volatile读和写的内存语义。
这里我们分别从编译器和处理器的角度来分析,CAS如何同时具有volatile读和volatile写 的内存语义。
注意:这里的state是一个volatile的变量!
前文我们提到过编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存橾作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义编译器不能对CAS与CAS前面和后面的任意内存操作重排序。
如何实现的?
常见的intel X86处理器中,这个本地方法在openjdk中依次调用的c++代码为:
unsafe.cpp, atomic.cpp和atomic一windows一x86.inline.hpp
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前 缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)。反之,如 果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
intel的手册对lock前缀的说明如下。
1)确保对内存的读-改-写操作原子执行。在Pentium&Pentium之前的处理器中,带有lock前 缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会 带来昂贵的开销。从Pentium4、Intel Xeon及P6处理器开始,Intel使用缓存锁定(Cache Locking) 来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。
2)禁止该指令,与之前和之后的读和写指令重排序。
3)把写缓冲区中的所有数据刷新到内存中。
上面的第2点和第3点所具有的内存屏障效果,足以同时实现volatile读和volatile写的内存语义。
CAS与内核态
CAS相当于在用户态代码里边插入了一个cmpxchg指令
直观看大概是这个样子:用户态内存空间[...你的代码你的代码cmpxchg你的代码你的代码...]
这样CPU一直在用户态执行,执行到cmpxchg指令也不是说就是切换内核态了,切换到内核态可以这么理解:就是CPU开始执行了内核态内存空间的操作系统的代码。
总结,CAS是没有发生用户态到内核态的切换的。只是在用户态执行了cmpxchg指令而已(这个指令由硬件保证原子性,所谓不可再分的CPU同步原语)。而执行指令要比上下文切换的开销要小,所以CAS要比重量级互斥锁性能要高。
然后说下重量级锁,直观看大概是这个样子:用户态空间[...你的代码你的代码你的代码lock] -> 执行操作系统内核态代码获得互斥锁(高低电位锁总线balabala)、返回互斥锁给用户态代码 -> 用户态空间[获得锁 你的代码你的代码...]
这样上述过程很显然发生了用户态到内核态切换了。