CAS
CAS
概述
compareAndSet
,也有(Compare And Swap
的说法),它必须时原子操作
是由硬件指令集支持的原子操作,原子行是由CPU
保证的,JVM
只是封装了汇编调用,这个操作需要输入两个数值,一个旧值(期望操作执行的值)和一个性质,在操作期间先比较旧值有没有改变,如果没有发生变化,才交换成新值
JDK
正式利用这些CAS
指令,可以实现并发的数据结构,比如AtomicInteger
等原子类
volatile:
- 获取共享变量时,为了保证该变量的可见性,需要volatile修饰
- 它可以用来修饰成员变量和静态变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存的。即一个线程对volatile变量的修改,对另一个线程可见
- **注意:**volatile仅仅保证了共享变量的可见性,让其他线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
- CAS必须借助volatile才能读取到共享变量的最新值来实现“比较并交换”的效果
为什么无锁效率高
- 无锁情况下,即使重试失败,线程始终再高速运行,没有停歇,而synchronized会让线程再没有获得锁的时候,发生上下文切换,进入阻塞
- 打个比喻:线程好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好像比赛车要减速、熄火,等待被唤醒又重新打火、启动、加速。。。恢复到高速运行,代价比较大
- 但无所情况下,线程要保持运行,需要额外CPU的支持,CPU在这里好比高速赛道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分配到时间片,仍然会进入可运行状态,还是会导致上下文切换
CAS特点
结合CAS和volatile可以实现无所并发,适用于线程数少、多核CPU的场景
- CAS
- 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没有关系,我吃亏点再重试呗
- synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
- CAS体现的是无锁并发、无阻塞并发
- 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
乐观锁问题
ABA问题
决定CAS
是否进行swap
的判断标准是“当前值是否和预期值相等”,如果一致,大多数情况下可以认为在此期间这个数值没有发生过变动。但是在此期间发生变动也是有可能的,比如A->B->A
,这个情况下是无法捕获到的CAS
只能检查出现在的值和最初的值是不是一样
问题解决方案:添加版本号
在atomic包中提供了AtomicStampedReference
这个类,他是专门用来解决ABA
问题的,解决思路正是利用版本号,AtomicStampedReference
会维护一个类似<Object,int>
的数据结构,其中int就是用来计数的,也就是版本号,它可以对这个对象和int
版本号进行原子更新,从而解决了ABA
问题
较大的循环开销
CAS
的第二个缺点就是自旋时间过长
由于单次CAS
不一定执行成功,所以CAS
往往配合着循环来实现,有时候甚至是死循环
如果我们的应用场景就是高并发的场景,就有可能导致CAS
一直操作不成功,这样的话,CPU
资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用CAS
,在高并发的场景下,通常CAS
效率是不高的
只能是单个变量的原子操作
对于CAS,我们不能针对多个共享变量同时进行CAS操作,因为这多个变量之间是相互独立的,简单的把原子操作组合到一起,并不具备原子性
我们想对多个对象进行CAS操作并想保证线程安全的话,是比较困难的。但是,我们可以使用AtomicReference来封装实现
1 | public class State{ |