一.什么是CAS
CAS: 全称 Compare and swap ,字面意思 :" 比较并交换 " ,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
我们来进行操作:
- 比较 V 和 A 是否相等。(比较)
- 如果比较是相等的,将B 写入 V。(交换)
- 返回操作是否成功。
伪代码如下:
java
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
这个就大概介绍了CAS的工作流程
二.CAS是如何实现的
针对不同的操作系统,JVM用到了不同的CAS实现原理
- java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
- Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
三.CAS的应用
1.实现原子类
我们都知道,在多线程环境下,我们的++操作不是线程安全的。那么我们的CAS就自己将 ++ 操作是现成了原子类。
java
import java.util.concurrent.atomic.AtomicInteger;
public class CAS {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
int ret = atomicInteger.incrementAndGet();
System.out.println(ret);
}
}
此时在Java中,为我们提供了这样的 ++ 原子类操作。以上代码的运行结果为 1 。
那么我们深究一下这其中的实现原理
先来看下面的伪代码:
java
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
首先要知道我们的CAS操作时原子的。在多线程环境下,那么他是如何保证每次读到的值,都是我们预期的值呢?
我们看下面的图片:
🎈第一步
两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)
🎈第二步
线程1进行CAS操作,先进行比较,发现 oldValue 和 Value 都是相同的,然后进行oldValue+1操作,最后再写入到内存中。此时主内存中Value的值为 1
🎈第三步
此时线程2进行操作。再进行 比较 oldValue 和 Value 是否相同的时候,发现不相同。此时重新进入while循环!在循环的时候, 会刷新oldValue的值-->1,此时oldValued 和 Value的值就相同了。正常进行+1赋值操作。
🎈第四步
线程 1 和 线程 2 返回各自的 oldValue 的值即可
我们可以很清楚的看到,CAS使用while循环巧妙的解决了++操作的线程安全问题。
2.实现自旋锁
先来看自旋锁的伪代码:
java
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;
}
}
自旋锁其实时一种锁的策略,它的实现原理就是如果当前的线程是有锁的,那么就一直while循环尝试获取锁,如果没锁了,那么就直接可以获取到CPU了。
四.CAS的ABA问题
我们先来看个例子:
假如我要去银行里取钱,我的存款是100元,我期望的是取出50元。
那么取款机创建了两个线程,假如线程1执行了-50的操作,线程2再执行的时候会报错。
1.正常的过程
- 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
望更新为 50. - 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
- 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.
2.异常的过程
- 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
望更新为 50. - 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
- 在线程2 执行之前, 我的朋友正好给滑稽转账 50, 账户余额变成 100 !!
- 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行了扣款操作!
那么此时就执行了两次扣款操作,这显然不是我们所期望的。
3.解决办法
给要修改的值 , 引入版本号 . 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期
版本号随着每次的使用而进行更新 。
还是刚才银行取钱的案例
为了解决 ABA 问题, 给余额搭配一个版本号 , 初始设为 1.
- 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100,
版本号为 1, 期望更新为 50. - 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
- 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.
- 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读
到的版本号为 1, 版本小于当前版本, 认为操作失败.
此时引入了版本号,ABA问题就完美的得到了解决。
总结:CAS是面试中常考的问题,我们需要深度学习,并且理解其中的操作机制。