详解线程安全机制—CAS
一、概述
CAS,全称 Compare-And-Swap
,翻译一下就是比较和交换,是一种乐观的并发控制机制。
CAS操作包含三个操作数:
- 内存地址(V):需要更新的变量的内存地址。
- 预期旧值(A):更新操作开始前的值。
- 新值(B):将要更新的目标值。
CAS操作的核心原理是:在更新某个变量的值时,会先比较内存地址(V)中的值是否与预期旧值(A)相等,如果相等,则将其更新为新值B,这个过程是原子的,即不可中断的。
举个例子:
内存地址(V)
中存放值为10的变量。- 线程A要把变量值加1,对于线程A来说,
预期旧值(A)
等于10,新值(B)
等于11。 - 在线程A执行之前,此时线程B提前将
内存地址(V)
中的变量值先更新成了11。 - 这时线程A执行CAS,首先会将
内存地址(V)
和预期旧值(A)
比较,发现不相等(V = 11,A = 10),提交失败。 - 线程A尝试自旋操作:重新获取
内存地址(V)
的值赋值给预期旧值(A)
,并重新计算想要修改的值,此时线程A的预期旧值(A)
等于11,新值(B)
等于12。 - 线程A重新提交更新,这时没有其他线程更新
内存地址(V)
中的值,此时内存地址(V)
的值和预期旧值(A)
相等,更新成功。
从上面的例子可以看出,CAS过程一定是一个原子操作,否则就没有意义了,那么如何保证CAS的原子性呢?
- 保证每次取内存地址(V)的值时,取到的一定是最新的,也就是变量需要使用
volatile
关键字修饰,volatile 保证了变量修改后一定会写回主内存,使用时也一定会从内存读取。 - 保证CAS的操作原子性,在Java中,CAS机制是通过
sun.misc.Unsafe
类中的方法实现的。Unsafe
类提供了底层的、不安全的操作,包括对内存的直接访问和修改。Java 的java.util.concurrent.atomic
包下的原子类(如AtomicInteger
、AtomicLong
等)就是基于CAS机制实现的。
使用场景:Android 中协程生命周期作用域 lifecycleScope
就是通过CAS来获取的,源码如下:
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (internalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
二、CAS的优势与劣势
优势:
- 非阻塞:CAS操作是一种非阻塞的算法,它不会使线程进入阻塞状态,因此不会引起线程上下文的切换和调度问题,这在高并发场景下非常有利。而 synchorized 关键字会让没有得到锁资源的线程进入Blocked状态,得到锁资源后恢复为Runnable状态,这个过程涉及到操作系统用户态到内核态的切换,代价比较高(其实 synchorized 后续也新增了自旋,轻量级锁,偏向锁等方案的优化)。
- 高性能:由于CAS操作是非阻塞的,且依赖于硬件的原子性保证,因此在很多情况下,CAS比传统锁性能更高。
劣势:
-
ABA问题:如果一个变量的值原来是1,被线程A读取后,线程B将其改为2,然后又改回1,此时线程A再次进行CAS操作时,会发现变量的值仍然是1,从而错误地认为该值没有被其他线程修改过。这就是所谓的ABA问题。
表面看没有什么影响,但是如果业务逻辑上是非幂等的,就会出现问题,比如银行业务由于ABA问题造成了多次扣款。
解决方案:同时维护一个变量的版本号,在Compare阶段不仅比较预期值和此时内存中的值,还比较两个比较变量的版本号是否一致,只有当版本号一致才进行后续操作,这样就解决了ABA问题。
-
开销大:在并发量比较高的情况下,如果许多线程反复尝试去更新一个变量,却又一直更新失败,循环往复,导致较大的开销。
-
只能保证一个共享变量的原子操作:CAS操作通常只能保证对单个共享变量的原子操作,对于涉及多个共享变量的复合操作,CAS就无能为力了,这时使用 synchorized 更加适合。
三、总结
从思想上来说,synchorized 属于悲观锁,悲观的认为程序中的并发多,所以严防死守,CAS机制属于乐观锁,乐观的认为程序中并发少,所以线程不断的去尝试更新。