![055ce3deba6dce18b3a6d3e90bfa8228.png](https://i-blog.csdnimg.cn/blog_migrate/a0ebd7c856e344e5eddc57b66a7b8951.jpeg)
我们知道并发编程离不开锁,以java为例,JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁。CAS即比较并替换,由计算机底层支持的原子性操作。本文将深入的介绍CAS的原理。
首先先讲一下在计算机内部是如何保证CAS的原子性的呢?
我们先看一下计算机缓存结构:
![33877140d35087e38619aaf8177a5120.png](https://i-blog.csdnimg.cn/blog_migrate/3b28f02cba121907854a88335e0fbf7e.png)
每个核心有其独有的L1和L2级高速缓存和共有的L3级缓存。
首先讲一下MESI协议:
Modified(M): 表示这个cacheline已经被修改,但是还未写回主存(cache中数据与主存中数据不一致!所有其它core对这个cacheline的读操作必须在该cacheline 写回主存后, 写回主存后,状态变换到share
Exclusive(E): 表示这个cacheline 目前是独有的且与主存一致,可以转换到shared 或者M,重点是可以转换到M,也就意味着可以对其修改!
Shared(S):表示这个cacheline 在其它core的cache中也存在, 目前也与主存一致, 随时会被invalidate.
Invalid(I): 表示这个cacheline 目前不可用
![7d1ac9497b6104b61733817e86c235bc.png](https://i-blog.csdnimg.cn/blog_migrate/cbe508177cc8d444c7406234c19041ab.png)
一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下会性能开销是相对较大的。在写入完成后,修改其缓存状态为M。
当两个核心使用CAS的方式对缓存行进行更改时,会向ring bus(在p6处理器之前,会采用锁总线的方式阻隔其它处理器访问内存,来达到原子性,在p6以后的X86处理器上,新加ring bus来传输每个核心对缓存行的修改,并通过MESI协议来达到原子性)发出invalidate这个操作。那么在ring bus上就会根据特定的设计协议仲裁是core0,还是core1能赢得这个invalidate,胜者完成操作, 失败者需要接受结果, invalidate自己对应的cacheline,再读取胜者修改后的值, 回到起点。对于我们的CAS操作来说, 其实锁并没有消失,只是转嫁到了ring bus的总线仲裁协议中. 而且大量的多核同时针对一个地址的CAS操作会引起反复的互相invalidate 同一cacheline, 造成pingpong效应, 同样会降低性能。
在x86架构上,CAS被翻译为”lock cmpxchg...“,首先一点cmpxchg并不是原子的,cmpchg会被拆解为:读取、比较、交换三个过程,也就是说如果两个cpu同时读取同时比较同时交换的话,会有协议仲裁谁能够成功,对于另一个cpu来说,也就意味着过程被打断了。因此可以使用lock前缀来锁住缓存行或者总线,保证排他性,在做比较变换的时候是原子性的。
那么可以说MESI是保证了可见性,但不保证原子性的(有待考究,不知道说的是否准确)
我们把视角调高,在应用层面上看看MESI:
volatile是java中的关键字,能够保证变量的可见性。有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
intel的手册对lock前缀的说明如下。
1)确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4、Intel Xeon及P6处理器开始,Intel使用缓存锁定(Cache Locking),刚才已经提到,来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。
2)禁止该指令与之前和之后的读和写指令重排序。
3)把写缓冲区中的所有数据刷新到内存中(可见性),对应缓存航标为M
而(3)(2)即内存屏障。
因此lock关键字保证了可见性和原子性。