Compare And Swap
Compare And Swap 就是经常听到的 CAS(比较和交换),它就是将多个原子操作(读-改-写)合并成一个原子操作。
举个例子:i++ 这个自增操作,它可以被拆分成三个操作,读 i 的值、i 的值加 1、写 i 的值;而 CAS 就可以将这三个原子操作,合并成一个原子操作。
比较和交换:先说一下比较,当写新数据到内存的时候,会先使用旧数据和当前内存中的值进行比较,如果一致就会将新值写入内存(就是交换),不一致就不会写入。
下面这张图来自美团技术团队,更好的帮助理解:

值得注意的是:CAS 必须配合 volatile 使用,这是因为 volatile 保证了可见性。还有就是 CAS 就是所谓的无锁编程,而在数据库中把把无锁编程叫做乐观锁。
下面是 Java 自带的一些支持原子操作的类,这些类都是使用 CAS 实现的:
- 基本类型:AtomicBoolean(布尔类型原子类)、AtomicInteger(整型原子类)、AtomicLong(长整型原子类)。
- 数组类型:AtomicIntegerArray(整形数组原子类)、AtomicLongArray(长整型数组原子类)、AtomicReferenceArray(引用类型数组原子类)。
- 引用类型:AtomicReference(引用类型原子类)、AtomicMarkableReference(带有标记位的引用类型原子类)、AtomicStampedReference(带有版本号的引用类型原子类)。
- 属性更新器类型:AtomicIntegerFieldUpdater(整型字段的原子更新器)、AtomicLongFieldUpdater(长整型字段的原子更新器)、AtomicReferenceFieldUpdater(原子更新引用类型里的字段)。
值得注意的是:其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性;而且与锁相比,CSA 性能消耗更少。
在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
ABA 问题
ABA 就是一个共享变量的值,变化来变化去(经过多次修改),又变化回了默认值(与默认值相同,就表示 “没有任何变化”);这样就导致了其它线程以为某件事情还没做,但其实已经做完了。
ABA 问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从 “A-B-A” 变成了 “1A-2B-3A”。JDK 中提供了 AtomicStampedReference 类来解决 ABA 问题,具体操作封装在 compareAndSet() 中。compareAndSet() 首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
这么做就是为了判断出,共享变脸的值被其它线程修改过。
除了 ABA 问题,CAS 还有以下两个问题:
- 循环时间长开销大。CAS 操作如果长时间不成功,会导致其一直自旋,给 CPU 带来非常大的开销。
- 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS 能够保证原子操作,但是对多个共享变量操作时,CAS 是无法保证操作的原子性的。但是从 1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性(也就是想将一个引用指向另一个对象时,并保证原子性,可以使用这个类),可以把多个变量放在一个对象里来进行 CAS 操作。
讨论
1. 两个线程同时对同一个共享变量执行 CAS 操作,另一个线程会等待吗?
另一个线程不会等待,因为 CPU 在执行 lock cmpxchg 指令的时候会锁住总线,所以我感觉是 CPU 级别的阻塞。
2. 锁有 ABA 问题吗?
我觉得,只要是同步就会出现 ABA 问题。
因为,ABA 问题就是共享变量的值经过多次修改又变成了默认值;当某个线程获取到锁,发现共享变量的值 “没有变化” 时,一样会去做一些操作。
参考资料
https://www.bilibili.com/video/BV1kE411u7bj?p=3
https://www.bilibili.com/video/BV1vt4y127Zq?from=search&seid=7717120777124817926
https://www.bilibili.com/video/BV1Sp4y1h7bu?from=search&seid=12658730434101202761

浙公网安备 33010602011771号