目录
- 🌴什么是 CAS
- 🌸CAS 伪代码
- 🎍CAS 是怎么实现的
- 🍀CAS 有哪些应⽤
- 🌸实现原子类
- 🌸实现自旋锁
- 🌳CAS 的 ABA 问题
- 🌸**什么是 ABA 问题**?
- 🌸ABA 问题引来的 BUG
- 🌸解决方案
- ⭕相关面试题
- 🚩总结
🌴什么是 CAS
CAS: 全称Compare and swap字⾯意思:”⽐较并交换“,⼀个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- ⽐较 A 与 V 是否相等。(⽐较)
- 如果⽐较相等,将 B 写⼊ V。(交换)
- 返回操作是否成功。
🌸CAS 伪代码
下⾯写的代码不是原⼦的, 真实的 CAS 是⼀个原⼦的硬件指令完成的. 这个伪代码只是辅助理解 CAS的⼯作流程.
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}
下面看两种典型的不是原子性的代码:
- check and set (if 判定然后设定值) [上⾯的 CAS 伪代码就是这种形式]
- read and update (i++) [之前我们讲线程安全的代码例⼦是这种形式]
CAS原子性的作用:
当多个线程同时对某个资源进⾏CAS操作,只能有⼀个线程操作成功,但是并不会阻塞其他线程,其他
线程只会收到操作失败的信号。
所以
CAS 可以视为是⼀种乐观锁. (或者可以理解成 CAS 是乐观锁的⼀种实现⽅式)
🎍CAS 是怎么实现的
针对不同的操作系统,JVM ⽤到了不同的 CAS 实现原理,简单来讲:
- java 的 CAS 利⽤的的是 unsafe 这个类提供的 CAS 操作;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
- Atomic::cmpxchg 的实现使⽤了汇编的 CAS 操作,并使⽤ cpu 硬件提供的 lock 机制保证其原⼦
性。
简⽽⾔之,是因为硬件予以了⽀持,软件层⾯才能做到。
🍀CAS 有哪些应⽤
🌸实现原子类
标准库中提供了 java.util.concurrent.atomic 包, ⾥⾯的类都是基于这种⽅式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
伪代码实现:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}}
假设两个线程同时调⽤ getAndIncrement
- 两个线程都读取 value 的值到 oldValue 中. (oldValue 是⼀个局部变量, 在栈上. 每个线程有⾃⼰的
栈)
- 线程1 先执⾏ CAS 操作. 由于 oldValue 和 value 的值相同, 直接进⾏对 value 赋值.
注意:
• CAS 是直接读写内存的, ⽽不是操作寄存器.
• CAS 的读内存, ⽐较, 写内存操作是⼀条硬件指令, 是原⼦的.
3. 线程2 再执⾏ CAS 操作, 第⼀次 CAS 的时候发现 oldValue 和 value 不相等, 不能进⾏赋值. 因此需要进⼊循环.
在循环⾥重新读取 value 的值赋给 oldValue
4. 线程2 接下来第⼆次执⾏ CAS, 此时 oldValue 和 value 相同, 于是直接执⾏赋值操作.
5. 线程1 和 线程2 返回各⾃的 oldValue 的值即可.
通过形如上述代码就可以实现⼀个原⼦类. 不需要使⽤重量级锁, 就可以⾼效的完成多线程的⾃增操作.
本来 check and set 这样的操作在代码⻆度不是原⼦的. 但是在硬件层⾯上可以让⼀条指令完成这个
操作, 也就变成原⼦的了.
🌸实现自旋锁
基于 CAS 实现更灵活的锁, 获取到更多的控制权.
⾃旋锁伪代码:
public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}
🌳CAS 的 ABA 问题
🌸什么是 ABA 问题?
通过下面的例子理解ABA问题:
假设存在两个线程 t1 和 t2. 有⼀个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使⽤ CAS 把 num 值改成 Z, 那么就需要
- 先读取 num 的值, 记录到 oldNum 变量中.
- 使⽤ CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是, 在 t1 执⾏这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, ⼜从 B 改成了 A
线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过⼜改成 A 了. 这个时
候 t1 究竟是否要更新 num 的值为 Z 呢?
到这⼀步, t1 线程⽆法区分当前这个变量始终是 A, 还是经历了⼀个变化过程.
这就好⽐, 我们买⼀个⼿机, ⽆法判定这个⼿机是刚出⼚的新⼿机, 还是别⼈⽤旧了, ⼜翻新过的⼿机.
🌸ABA 问题引来的 BUG
⼤部分的情况下, t2 线程这样的⼀个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除⼀些特殊情况.
假设 滑稽⽼哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执⾏ -50 操 作.
我们期望⼀个线程执⾏ -50 成功, 另⼀个线程 -50 失败. 如果使⽤ CAS 的⽅式来完成这个扣款过程就可能出现问题.
正常的过程:
- 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更
新为 50. - 线程1 执⾏扣款成功, 存款被改成 50. 线程2 阻塞等待中.
- 轮到线程2 执⾏了, 发现当前存款为 50, 和之前读到的 100 不相同, 执⾏失败.
异常的过程
- 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更
新为 50. - 线程1 执⾏扣款成功, 存款被改成 50. 线程2 阻塞等待中.
- 在线程2 执⾏之前, 滑稽的朋友正好给滑稽转账 50, 账⼾余额变成 100 !!
- 轮到线程2 执⾏了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执⾏扣款操作
这个时候, 扣款操作被执⾏了两次!!! 都是 ABA 问题搞的⻤!!
🌸解决方案
给要修改的值, 引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期.
• CAS 操作在读取旧值的同时, 也要读取版本号.
• 真正修改的时候,
- 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
- 如果当前版本号⾼于读到的版本号. 就操作失败(认为数据已经被修改过了).
这就好⽐, 判定这个⼿机是否是翻新机, 那么就需要收集每个⼿机的数据, 第⼀次挂在电商⽹站上的⼿
机记为版本1, 以后每次这个⼿机出现在电商⽹站上, 就把版本号进⾏递增. 这样如果买家不在意这是翻
新机, 就买. 如果买家在意, 就可以直接略过.
对⽐理解上⾯的转账例子
假设 滑稽⽼哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执⾏ -50 操 作.
我们期望⼀个线程执⾏ -50 成功, 另⼀个线程 -50 失败. 为了解决 ABA 问题, 给余额搭配⼀个版本号, 初始设为 1.
- 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100, 版本
号为 1, 期望更新为 50. - 线程1 执⾏扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
- 在线程2 执⾏之前, 滑稽的朋友正好给滑稽转账 50, 账⼾余额变成 100, 版本号变成3.
- 轮到线程2 执⾏了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读到的版本号为 1, 版本⼩于当前版本, 认为操作失败.
在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进⾏包装, 在内
部就提供了上⾯描述的版本管理功能.
关于 AtomicStampedReference 的具体⽤法此处不再展开. 有需要的同学⾃⾏查找⽂档了
解使⽤⽅法即可.
⭕相关面试题
- 讲解下你⾃⼰理解的 CAS 机制
全称 Compare and swap, 即 “⽐较并交换”. 相当于通过⼀个原⼦的操作, 同时完成 “读取内存, ⽐较是
否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的⽀撑
2.ABA问题怎么解决?
给要修改的数据引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期. 如
果发现当前版本号和之前读到的版本号⼀致, 就真正执⾏修改操作, 并让版本号⾃增; 如果发现当前版
本号⽐之前读到的版本号⼤, 就认为操作失败
🚩总结
关于《【多线程】CAS详解》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!