加锁过于残暴?不妨试试CAS

大家好,我是徒手敲代码。

今天来介绍一下CAS。这个东西的全名是 Compare and Swap,比较并交换。在修改数据之前,先比较指定内存位置上的值,看是否相等,如果相等,就用新的值替换旧值,如果不相等,那么就直接返回。

ABA问题

首先来想象一个场景:假设有一个共享变量 x,值为 A。

  • 线程1 准备更新 x 的值,获取到 x 的值为A,准备执行CAS操作

  • 此时线程2 也进来了,将 x 的值改为 B,然后又将 x 的值改回 A

  • 线程1执行 CAS 操作,发现 x 的值还是 A,于是判定为值没有发生过变化,执行修改操作

这就是典型的 ABA 问题,在一些场景中,变量的每一次变化都是有意义的,不能当作看不见。比如引用计数的场景,变量的每一次变化都是唯一的。

为了解决这个问题,可以对变量加个版本号,每次修改的时候,把版本号也改一下,比如 JUC atomic 包下的AtomicStampedReference类。

原子性

从名字可以看出来,CAS包含两个操作,比较和交换,那么这两个操作如何保证原子性呢?

通过操作系统底层,硬件级别的支持,主要利用总线锁定缓存锁定这两个机制来完成。假设使用的处理器都是多核的:

  • 总线锁定,处理器会向总线发送一个锁定信号Lock,这个信号会通知其他处理器,当前处理器正在处理一个原子操作,其他处理器的请求先阻塞,然后等当前处理器完成操作之后,释放锁定信号,其他处理器才能访问共享内存。这种锁定方式直接把 CPU 和内存的通信给锁住了

  • 缓存锁定,依赖于缓存一致性协议,当处理器想要修改共享缓存行的数据,要先获取这一行的锁,然后执行CAS操作,更新完成的数据会传输到其他处理器的缓存中,确保所有处理器看到的都是最新的数据

不同品牌,或者同一品牌不同架构的CPU,保证 CAS 原子性的机制是不一样的,常见的IntelAMD,其x86/x64架构处理器广泛采用了上面提到的总线锁定和缓存锁定机制。

适用场景

CAS 是乐观锁的一种实现,它无需阻塞线程等待锁释放,减少了线程间的直接竞争和上下文切换的成本,特别适合那些对性能要求极高的并发场景。

实际上,如果 CAS 判断出目标值曾经被修改过,那么进行自旋重试的操作。如果长时间重试不成功,就会白白浪费 CPU 的资源。同时 CAS 只能保证一个共享变量的原子操作。当需要对多个共享变量操作时,就要考虑使用锁,或者把多个变量放在一个对象里,使用 AtomicReference 保证引用对象操作的原子性。

优化

针对自旋次数过多导致性能的问题,可以对线程加入自旋次数的限制,超过这个限制就放弃操作。

jdk1.8中,引入了LongAdder这个类,通过分段的思想,将一个大的计数器拆分成多个小的单元(分段),每个单元独立进行CAS操作。就相当于,本来是一群人向一个箱子里面交保护费,要排很长的队(相当于自旋)。现在是放了多个小盒子,每个人可以选择最近的小盒子去投放,而不至于一直等。最后操作完的小盒子,再汇总成一个大箱子,得到最终的统计结果。

今天的分享到这里结束了。

关注公众号“徒手敲代码”,免费领取由腾讯大佬推荐的Java电子书!

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值