文章目录
- CAS
- 1. 什么是 CAS
- 2. CAS 是怎么实现的
- 3. CAS 有哪些应用
- 3.1 实现原子类
- 3.2 实现自旋锁
- 4. CAS 的 ABA 问题
- 4.1 什么是 ABA 问题
- 4.2 ABA 问题引来的 BUG
- 4.3 解决方案
- 5. 相关面试题
CAS
1. 什么是 CAS
CAS:全称 Compare and swap,字面意思:”比较并交换“。
一个 CAS 涉及到以下操作:
我们假设内存中的 原数据V,旧的预期值A,需要修改的新值B。
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
举个例子。
2. CAS 是怎么实现的
此处最特别的地方,上述这个 CAS 的过程并非是通过一段代码实现的,
而是通过一条 CPU 指令完成的。
因为 CAS 操作是原子的,所以就可以在一定程度上回避线程安全问题。
解决线程安全问题,除了加锁之外,又有了一个新的方向了。
CAS 可以理解为是 CPU 给咱们提供的一个特殊指令,通过这个指令,就可以一定程度上处理线程安全问题。
观察下面一段伪代码(只是辅助理解CAS 的工作流程)
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
address 就相当于是上面例子的 V,expectValue 相当于是 A
swapValue 相当于是 B。
3. CAS 有哪些应用
3.1 实现原子类
java 标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。
典型的就是 AtomicInteger 类。
其中:
getAndIncrement 相当于 i++ 操作,
incrementAndGet 相当于是 ++i 操作。
getAndDecrement 相当于是 i-- 操作,
decrementAndGet 相当于是 --i 操作。
package thread;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo8 {
public static void main(String[] args) throws InterruptedException{
// 这些原子类,就是基于 CAS 实现了自增,自减操作,此时进行这类操作不加锁,也是线程安全的
AtomicInteger count = new AtomicInteger(0);
// 使用原子类来解决线程安全问题
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();// 相当于是 count++
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();// 相当于是 count++
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get());
}
}
因为进行这类操作不加锁是,也是线程安全的,所以代码会输出 10000。
针对上面 count++ 操作的伪代码实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
代码分析
箭头从上到下代表执行的顺序。
1、t1 先执行 load ,把 value 的值读到 oldvalue 中。
读取后
2、执行 t2 的 load 操作,把 value 的值 读到 oldvalue 中
执行后
3、t2 执行 CAS 操作比较 oldvalue 和 value 的值是否相等
此时 value 与 oldvalue 的值相等,就将 oldValue+1 的值读给 value。
读取后
4、t1 执行 CAS 操作比较 oldvalue 和 value 的值是否相等
此时 value 与 oldvalue 的值不相等,CAS 返回 false,并且不进行任何交换。
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
针对于上述的代码,CAS 返回 false 后,进入 while 循环,将 value 的值 读给 oldvalue。
紧接着 t1 就要再接着执行一次 load 和 CAS。
读取后。
5、t1 执行 CAS 操作比较 oldvalue 和 value 的值是否相等
此时 value 与 oldvalue 的值相等,就将 oldValue+1 的值读给 value。
读取后
此时比较相等,CAS 返回 true ,循环就结束了。
上面的伪代码要结合下面的图示来理解。
3.2 实现自旋锁
基于 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;
}
}
this.owner, null 监测当前的 owner 是否是 null ,如果是 null就进行交换,
也就是把当前线程的引用赋值给 owner 。
如果赋值成功,此时循环结束,加锁完成了。
如果当前锁已经被别的线程占用了,CAS 就会发现 this.owner 不是 null。
CAS 就不会产生赋值,也同时返回 false ,循环继续执行,并且下次判定。
4. CAS 的 ABA 问题
4.1 什么是 ABA 问题
CAS 在运行中的核心,检查 value 和 oldvalue 是否一致。
如果一致就视为 value 中途没有被修改过,所以进行下一步操作是没有问题的。
这里指的一致,可能是没有改过,也可能改过之后又还原回来了。
比方说,把 value 的值设为 A 的话,CAS 判定 value 为 A 此时可能确实 value 始终是 A。
也可能是 value 本来是 A ,被改成了 B 后又改回了 A 。
举个例子。
就像是买手机,我买到的这个手机,可能是翻新机,也可能是新机。
不管是哪一种,我都无法区分。
4.2 ABA 问题引来的 BUG
ABA 这个情况大部分情况下其实是不会对代码逻辑产生太大影响的。
但是不排除一些比较极端情况,也是可能造成影响的。
下面举一个在实际开发环境中发生概率不大的例子
假设当前滑稽老铁的账户余额 为 1000 ,滑稽准备去 500 。
当按下取款这一瞬间的时候,机器卡了一下,滑稽老铁忍不住就多按了几下。
这是可能就会产生 bug ,可能就会触发重复扣款的操作。
考虑使用 CAS 的方式来扣款。
t1 和 t2 都执行 load 操作,都读取到了 1000 这个值。
紧接着 t1 执行 CAS 比较一下,看看此时的余额是不是 1000,如果是 1000 就扣 500。
当 t2 执行 CAS 操作的时候,滑稽的余额为 500 不等于 1000,此时扣款不成功。
如果在执行果第一个 CAS 后,有人给滑稽转了 500 元,此时余额又变成了 1000。
这就相当于,本来是 1000 变成 500 后 又变成了 1000 ,这就是一个ABA 问题。
此时的余额是 1000 ,就又扣款成功了,这里就出现 bug 了。
4.3 解决方案
针对当前的问题,采取的方案就是 加一个版本号
比方说,初始版本号是 1 ,每次修改版本号都加1,然后进行 CAS 的时候,不是以金额为准了,而是以版本号为准。
版本号真是增长的,不能降低。此时只要是版本号没变,就是一定没有发生改变。
5. 相关面试题
1、讲解下你自己理解的 CAS 机制
全称 Compare and swap,即 “比较并交换”。
相当于通过一个原子的操作,同时完成 “读取内存,比较是否相等,修改内存” 这三个步骤。
本质上需要 CPU 指令的支撑。
2、ABA问题怎么解决
给要修改的数据引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。
如果发现当前版本号和之前读到的版本号一致,就真正执行修改操作,并让版本号自增;如果发现当
前版本号比之前读到的版本号大,就认为操作失败。