什么是 CAS
CAS 即 Compare And Swap 的缩写, 翻译成中文就是比较并交换, 其作用是让 CPU 比较内存中某个值是否和预期的值相同, 如果相同则将这个值更新为新值, 不相同则不做更新, 也就是 CAS 是原子性的操作, 其实现方式是通过借助 C/C++ 调用 CPU 指令完成的, 所以效率很高.
CAS 的原理很简单, 这里使用一段 Java 代码来描述
- public boolean compareAndSwap(int value, int expect, int update) {
- // 如果内存中的值 value 和期望值 expect 一样 则将值更新为新值 update
- if (value == expect) {
- value = update;
- return true;
- } else {
- return false;
- }
- }
大致过程是将内存中的值, 我们的期望值, 新值交给 CPU 进行运算, 如果内存中的值和我们的期望值相同则将值更新为新值, 否则不做任何操作. 这个过程是在 CPU 中完成的, 这里不好描述 CPU 的工作过程, 就拿 Java 代码来描述了.
Unsafe 源码分析
Java 是在 Unsafe(sun.misc.Unsafe)类实现 CAS 的操作, 而我们知道 Java 是无法直接访问操作系统底层的 API 的(原因是 Java 的跨平台性限制了 Java 不能和操作系统耦合), 所以 Java 并没有在 Unsafe 类直接实现 CAS 的操作, 而是通过 **JDI(Java Native Interface)** 本地调用 C/C++ 语言来实现 CAS 操作的.
Unsafe 有很多个 CAS 操作的相关方法, 这里举例几个
- public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
- public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
- public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
我们拿 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 进行分析, 这个方法是比较内存中的一个值 (整型) 和我们的期望值 (var4) 是否一样, 如果一样则将内存中的这个值更新为 var5, 参数中的 var1 是值所在的对象, var2 是值在对象 (var1) 中的内存偏移量, 参数 var1 和参数 var2 是为了定位出值所在内存的地址.
Unsafe.java 在这里发挥的作用有:
将对象引用, 值在对象中的偏移量, 期望的值和欲更新的新值传递给 Unsafe.cpp
如果值更新成功则返回 true 给开发者, 没有更新则返回 false
Unsafe.cpp 在这里发挥的作用有:
接受从 Unsafe 传递过来的对象引用, 偏移量, 期望的值和欲更新的新值, 根据对象引用和偏移量计算出值的地址, 然后将值的地址, 期望的值, 欲更新的新值传递给 CPU
如果值更新成功则返回 true 给 Unsafe.java, 没有更新则返回 false
CPU 在这里发挥的作用:
接受从 Unsafe.cpp 传递过来的地址, 期望的值和欲更新的新值, 执行指令 cmpxchg, 比较地址中的值是否和期望的值一样, 一样则将值更新为新的值, 不一样则不做任何操作
将操作结果返回给 Unsafe.cpp
CAS 的缺点
CAS 虽然高效的实现了原子性操作, 但是也存在一些缺点, 主要表现在以下三个方面.
ABA 问题
在多线程场景下 CAS 会出现 ABA 问题, 关于 ABA 问题这里简单科普下, 例如有 2 个线程同时对同一个值 (初始值为 A) 进行 CAS 操作, 这三个线程如下
线程 1, 期望值为 A, 欲更新的值为 B
线程 2, 期望值为 A, 欲更新的值为 B
线程 1 抢先获得 CPU 时间片, 而线程 2 因为其他原因阻塞了, 线程 1 取值与期望的 A 值比较, 发现相等然后将值更新为 B, 然后这个时候出现了线程 3, 期望值为 B, 欲更新的值为 A, 线程 3 取值与期望的值 B 比较, 发现相等则将值更新为 A, 此时线程 2 从阻塞中恢复, 并且获得了 CPU 时间片, 这时候线程 2 取值与期望的值 A 比较, 发现相等则将值更新为 B, 虽然线程 2 也完成了操作, 但是线程 2 并不知道值已经经过了 A->B->A 的变化过程.
ABA 问题带来的危害:
小明在提款机, 提取了 50 元, 因为提款机问题, 有两个线程, 同时把余额从 100 变为 50
线程 1(提款机): 获取当前值 100, 期望更新为 50,
线程 2(提款机): 获取当前值 100, 期望更新为 50,
线程 1 成功执行, 线程 2 某种原因 block 了, 这时, 某人给小明汇款 50
线程 3(默认): 获取当前值 50, 期望更新为 100,
这时候线程 3 成功执行, 余额变为 100,
线程 2 从 Block 中恢复, 获取到的也是 100,compare 之后, 继续更新余额为 50!!!
此时可以看到, 实际余额应该为 100(100-50+50), 但是实际上变为了 50(100-50+50-50)这就是 ABA 问题带来的成功提交.
解决方法: 在变量前面加上版本号, 每次变量更新的时候变量的版本号都 + 1, 即 A->B->A 就变成了 1A->2B->3A.
循环时间长开销大
如果 CAS 操作失败, 就需要循环进行 CAS 操作(循环同时将期望值更新为最新的), 如果长时间都不成功的话, 那么会造成 CPU 极大的开销.
这种循环也称为自旋
解决方法: 限制自旋次数, 防止进入死循环.
只能保证一个共享变量的原子操作
CAS 的原子操作只能针对一个共享变量.
解决方法: 如果需要对多个共享变量进行操作, 可以使用加锁方式 (悲观锁) 保证原子性, 或者可以把多个共享变量合并成一个共享变量进行 CAS 操作.
CAS 的应用
我们知道 CAS 操作并不会锁住共享变量, 也就是一种非阻塞的同步机制, CAS 就是乐观锁的实现.
乐观锁 乐观锁总是假设最好的情况, 每次去操作数据都认为不会被别的线程修改数据, 所以在每次操作数据的时候都不会给数据加锁, 即在线程对数据进行操作的时候, 别的线程不会阻塞仍然可以对数据进行操作, 只有在需要更新数据的时候才会去判断数据是否被别的线程修改过, 如果数据被修改过则会拒绝操作并且返回错误信息给用户.
悲观锁 悲观锁总是假设最坏的情况, 每次去操作数据时候都认为会被的线程修改数据, 所以在每次操作数据的时候都会给数据加锁, 让别的线程无法操作这个数据, 别的线程会一直阻塞直到获取到这个数据的锁. 这样的话就会影响效率, 比如当有个线程发生一个很耗时的操作的时候, 别的线程只是想获取这个数据的值而已都要等待很久.
Java 利用 CAS 的乐观锁, 原子性的特性高效解决了多线程的安全性问题, 例如 JDK1.8 中的集合类 ConcurrentHashMap, 关键字 volatile,ReentrantLock 等.
参考
JAVA CAS 原理深度分析 https://zl198751.iteye.com/blog/1848575
Java CAS 原理分析
什么是 ABA 问题? https://www.zhihu.com/question/23281499
来源: https://juejin.im/post/5c87afa06fb9a049f1550b04