cas缓存锁
‘壹’ 原子操作的实现原理
我们一起来聊一聊在Inter处理器和java里是如何实现原子操作的。
32位IA-32处理器使用基于 对缓存加锁或总线加锁 的方式来实现多处理器之间的原子操作
首先处理器会自动保证基本的内存操作的原子性。 处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
第一个机制是通过总线锁保证原子性。 如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图
处理器使用总线锁就是来解决这个问题的。 所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
“缓存锁定”指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不需要在总线上声言LOCK#信号,而是修改内部的内存地址,通过缓存一致性机制保证操作的原子性。
例外:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行,处理器会调用总线锁定。
在java中可以通过锁和循环CAS的方式来实现原子操作。
CAS
ABA问题
循环时间长开销大
只能保证一个共享变量的原子操作
原子操作的实现原理
聊聊并发(五)原子操作的实现原理
‘贰’ 几句话总结CAS锁与synchronized差异
其实网上有很多做过实验对比的,这里就不一一叙述了,
总的来说就是,竞争少的情况喊伍汪下CAS优于synchronized,反之synchronized优于CAS。
刚开始接触CAS的时候,一听说是锁,还是非阻塞的,就觉得肯定比加锁快,其实不然,还是得透过现象看本质,不要管中窥豹。
我尝试用几句话总结:
根据上述,我们可以知道,CAS失败的情况下,自旋锁会一直旋转,synchronized虽然不会旋转,但是在获得锁的情况下,除了异常,其他情况下是肯定成功的。
所以两者的时间差别就是,自旋的时间和线程上下文切换的时间,虽然在其他资源在充足情况下,自旋成功下的时间肯定比线郑仔程加锁要来的快,但是你考虑橘粗下,如果竞争很激烈的情况下,是不是只有一个CAS能成功,那么其他的都是失败自旋,那么在这种情况下,CPU的资源的消耗可以说十分大的,那我还不如给这个共享资源加锁,剩下的线程等待不占用CPU资源,这种情况下,你可以考虑负载均衡,你可以考虑在别的情况下并行或者并发处理别的事务,都可以,否则如果遇到stop world 的情况就得不偿失了。
希望大家指点和交流,谢谢!
‘叁’ CAS在项目中的应用
模拟一般抢购活动中需要加锁的程序,采用synchronized锁与CAS锁,比较二者的性能。说明二者的区别,并指出合适应该使用CAS锁。
首先创建一个spring boot 的项目,不修改任何默认tomcat的配置,模拟抢购程序耗时100ms
这些程序的失败只是说明在压测是时间内程序没有响应(5s以上),后台继续在运行,用监控工具可以看到
实际上处理完1000个线程耗时的时间是两分含族首钟(visualVM反应有点小迟钝),并且可以大致看出spring boot 默认的线程数大概在200(还有一些程序存谈数活的守护线程,我们并不能利用起来),在内存时序图中,可以大致看到,新建一个线程,大概消耗10m的内存。
这些意味着如果你采用synchronized,两分钟内,你的服务处于假死状态(tomcat的线程被占完),不做测试的话其实也能估算出大概的耗时,毕竟主要就是抢购程序内部耗时,每个100ms,1000个就是两分钟左右,加锁的性能很低,不到万不得已不要加重锁。
换成CAS的操作
根据测试的结果,可以大致判断CAS锁的使用场景:快速响应失败。比如会穗茄场活动,或者需要连续点击的抢购,不需要按时间点击顺序(这个需要mq的帮助)确定抢购结果的活动。
‘肆’ 每日一问:谈谈 synchronized 和 CAS 机制
昨天的文章 我们针对 Java 语言的 "happends-before" 原则做了一个非常简单的表述,以致于有同学提到我这个话语的严谨性问题。而这个原则在 Java 语言里面非常重要,以致于我必须重新引用一下相关书籍的话来进行论述。
"happends-before" 先行发生原则是 Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先行发生于操作 B,那么操作 A 产生的影响一定应该被操作 B 所观察到。
而对于我们 volatile 保证的可见性, synchronized 和 final 关键字也同样可以做到。那我们今天就来简单讲一下我们非常常用的 synchronized 和似乎在 Android 中少有听到的 CAS 机制。
synchronized 采用的是 CPU 悲观锁机制,即线程获得的是独占锁。独占锁就意味着 其他线程只能依靠阻塞来等待线程释放锁 。而在 CPU 转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起 CPU 频繁的拦碧上下文切换导致效率很低。尽管 Java1.6 为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的正早过度,但是在最终转变为重量级锁之后,性能仍然简清举较低。
CAS 是英文单词 Compare And Swap 的缩写,翻译过来就是比较并替换。它当中使用了3个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。采用的是一种乐观锁的机制,它不会阻塞任何线程,所以在效率上,它会比 synchronized 要高。所谓乐观锁就是: 每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
所以,在并发量非常高的情况下,我们尽量的用同步锁,而在其他情况下,我们可以灵活的采用 CAS 机制。
在 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。例如 AtomicBoolean , AtomicInteger , AtomicLong 等,它们就是典型的利用 CAS 机制实现的原子操作类。
此外, Lock 系列类的底层实现以及 Java 1.6 在 synchronized 转换为重量级锁之前,也会采用到 CAS 机制。
关于 CAS 机制的更多相关信息请移步:
漫画:什么是CAS机制?(进阶篇)
‘伍’ CAS原理以及CAS带来的三大问题
参考: https://www.jianshu.com/p/ab2c8fce878b
https://www.jianshu.com/p/68f9cd012de8
CAS :Compare and Swap,即比较再交换。
CAS算法理解 :CAS是一种无锁算法,CAS有3个操作数,内存值E,旧的预期值V,要修改的新值N。当且仅当预期值V和内存值E相同时,将内存值E修改为N,否则什么都不做。
CAS算法图解 :
上图描述了CAS的原理,以及带来的三大问题以及问题出运粗现的位置。
1.ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查的时候发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前的标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值。
2.循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,如果JVM能旁雀镇支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在循环的岁亮时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空,从而提高CPU的实行效率。
3.只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ji=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之前的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
