CAS面试提问环节:
- synchronized、ReentrantLock、CAS全家桶发售
- HashMap、Hashtable、ConcurrentHashMap组合拳出击
CAS简介
比较并交换(compare and swap, CAS),是原子操作的一种。在多线程没有锁的状态下,可以保证多个线程对同一个值的更新。
CAS可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性,产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
CAS的特点
- CAS结合volatile可以实现无锁并发,适用于线程数少,多核CPU场景下(单核CPU下while自旋纯属浪费时间)。
- CAS体现着乐观锁思想(本身并无锁🔒,区别于synchronized)
- CAS体现的是无锁并发、无阻塞并发
- CAS的原子性 + volatile的可见性,不断的的【比较于交换】保证线程安全
- 没有用锁🔒来保证线程安全,所以不会阻塞
- 如果竞争激烈,会导致重试频繁发生,效率下降
CAS原理:自旋–比较-交换
CAS 操作中包含三个操作数 —— 内存位置 (V) 、预期原值 (E) 和新值(N)
这三个字母缩写的含义: Variable:变量;Expect:预期;New:新值
我们都知道,多个线程在访问共享资源的时候,会产生同步问题,所以需要加锁来保证安全。但是,一旦加了锁,同一时刻只能有一个线程获取锁对象,效率自然变低了。
那在多线程场景下,不加锁的情况下来修改值,CAS是怎么自旋的呢?
【用图举例来说明】
- 现在Data中存放的是num=0,线程A将num=0拷贝到自己的工作内存中计算(做+1操作)E=0,计算的结果为V=1
- 由于是在多线程不加锁的场景下操作,所以可能此时num会被别的线程修改为其他值。此时需要再次读取num看其是否被修改,记再次读取的值为N
- 如果被修改,即E != N,说明被其他线程修改过。那么此时工作内存中的E已经和主存中的num不一致了,根据EMSI协议,保证安全需要重新读取num的值。直到E = N才能修改
- 如果没被修改,即E = N,说明没被其他线程修改过。那门将工作内存中的E=0改为E=1,同时写回主存。将num=0改为num=1
再来一个经典的转账例子:
要在当前状态的余额下减10
什么是ABA问题
举个不恰当的例子🙃。张三和他的女朋友李四分手了,经历了全球变暖以及黄金的贬值的重重考验和磨难后最终又复合。那么,在分手期间,张三是不知道李四有没有重新找过男朋友的。但是,最终的男盆友是自己就行了!这就是ABA问题。
回归到问题本身:
在线程A计算V的时候,可能线程B将num=0改为了num=2,线程C又将num=2改回了num=0(也有可能是线程B或这任意线程)。此时num的值虽然还时0,但是num已经不是一开始的num了。
用一句哲学的话来讲,就是:“世界上没有两片相同的树叶”!
ABA问题怎么解决
CAS是无法解决ABA问题的。解决的策略有哪些呢?
添加版本号
大家在更新手机APP的时候,每次更新时软件都会有版本号。你更新完软件,还是原来的应用,但是版本号就不一样了。
同样,ABA问题也可以这样来求解。
我们把值num加一个版本号tag,当有线程修改的时候,版本号就会发生变化。在读取E的时候,同时将版本号tag也读取上,在比较E=?=N的时候,同时比较tag是否发生了改变。
添加时间戳
添加世时间戳也可以解决。查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间,这样也能保证,方法很多但是跟版本号都是异曲同工之妙。
CAS锁升级
针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式:
- 先使用偏向锁优先同一线程,然后再次获取锁
- 如果失败,就升级为 CAS 轻量级锁
- 如果失败就会短暂自旋,防止线程被系统挂起
- 最后如果以上都失败就升级为重量级锁
CAS的优点与缺点
优点:
在多核CPU且并发量不是很高的时候可以提高效率。
缺点:
- 如果长时间循环对CPU的开销很大
- CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。
- ABA问题
- 不能保证变量的内存可见性。CAS获取共享变量的值时,需要和volatile配合使用,来保证共享变量的可见性
CAS 实现自旋锁
既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁(实际是无锁),它实际上是直接利用了 CPU 层面的指令,所以性能很高。
CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果。
这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
扩展:其实 JDK 中有好多地方用到了 CAS ,尤其是java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到过 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
AtomicBoolean 中的CAS
1 | public class AtomicBoolean implements java.io.Serializable { |
- 使用了
sun.misc.Unsafe
对象,这个类提供了一系列直接操作内存对象的方法,只是在 jdk 内部使用,不建议开发者使用; value
表示实际值,可以看到 get 方法实际是根据 value 是否等于0来判断布尔值的,这里的 value 定义为 volatile,因为 volatile 可以保证内存可见性,也就是 value 值只要发生变化,其他线程是马上可以看到变化后的值的;valueOffset
是 value 值的内存偏移量,用 unsafe.objectFieldOffset 方法获得,用作后面的 compareAndSet 方法;compareAndSet
方法,这就是实现 CAS 的核心方法了,在使用 AtomicBoolean 的这个方法时,只需要传递期望值和待更新的值即可,而它里面调用了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,用 c++ 实现,具体的代码就不贴了,总之是利用了 CPU 的 cmpxchg 指令完成比较并替换,当然根据具体的系统版本不同,实现起来也有所区别,感兴趣的可以自行搜一下相关文章。- CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果