1.CAS的概念与实现
CAS的全称是 compare and swap
涉及到的操作是寄存器A中的值与内存的值M进行比较,如果值相同就把寄存器B的值和M进行交换
用一段伪代码来理解一下
address 是内存地址 代表 M
expectValue 代表寄存器 A
swapValue 代表寄存器 B
他先判断一下内存地址的值 M 和接收到的值确认一下是不是一样的,如果一样就将值进行交换并且返回一个 true
CAS 操作是一条 CPU 指令,比不是上述一段代码,相当于 CAS 操作是原子的
2.CAS 的应用
CAS 相当于我们不需要进行加锁也能实现很多操作,下面举两个例子
(1) 原子类
Java标准库提供了 Atomlnteger 类,可以保证 ++ 和 -- 的操作线程安全
举个例子先
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
System.out.println(atomicInteger.getAndIncrement());//打印0
int val = atomicInteger.get();
System.out.println(val);//打印1
}
还是使用一段伪代码来解释一下,比较好描述这一段
这个操作相当于 i++;
其中的 value 相当于是内存中最开始存储的数字,也就是我们代码中的初始值 0
oldvalue 相当于一个寄存器( Java 中没法表示寄存器的值,只能用变量表示),先存储一下内存的值也就是 0
(1) while 这个是利用 CAS 操作来判断 value 和 oldvalue 是否相同,如果相同的话就返 true ,此时循环不会执行,并且 value++ ,最终返回原来 oldValue 的值也就是0
最终就是value被修改成 1 ,返回oldValue也就是 0
(2) 如果CAS发现 value 和 oldValue不相等的话返回 false ,循环开始执行,更新一下 oldValue 的值,如果此时发现相等就会重复 (1) 的操作
此处的CAS就是在确认 value 的值是不是有人动过,如果动过,就更新一下,没动过就在内存值 ++ 一下
(2) 自旋锁
先放一段伪代码来分析自旋锁,在 synchronized 里由偏向锁向轻量级锁转变后,就会产生自旋锁
public class SpinLock {
private Thread owner = null; // 表示当前持有锁的线程,初始为 null 表示没有线程持有锁。
public void lock() {
// 通过 CAS 看当前锁是否被某个线程持有。
// 如果这个锁已经被别的线程持有,那么就自旋等待。
// 如果这个锁没有被别的线程持有,那么就把 owner 设置为当前尝试加锁的线程。
while (!CAS(this.owner, null, Thread.currentThread())) {
// 自旋等待,直到 CAS 成功
}
}
public void unlock() {
this.owner = null; // 释放锁,将 owner 置为 null。
}
}
owner 表示当前锁是否有被持有
CAS 操作时表示如果当前的 owner 为 null ,比较就成功,将当前线程的引用地址放到 owner 上,完成几所
如果比较不成功,说明 owner 被人持有, CAS 就会返回 false , while 循环就会一直进行,知道 owner 被unlock 设为 null 的时候,CAS操作返回 true 时,进行加锁
好处 : 一旦锁释放立刻就能拿到
坏处 : cpu 会一直处在忙等状态,浪费cpu的资源
3. ABA 问题
由于 CAS 是进行对值的比较,有时候 value 和 oldValue 对比是相同的,但是不代表没变过
而是从a -> b -> a,此时有一定概率会出现一些 bug
例子
比如现在有一个共享变量 value 的值是 A
有三个线程 : t1,t2,t3
时间点1. 线程 t1 读取 value 的值并准备执行 CAS 操作
当 t1 读取 value 的值,存储到 oldValue 时(此时这个操作并不是原子的), t1 的目的是将 A 修改成 C
时间点2. 线程 t2 更新 value 的值
线程 t2 读取了 value 的值为 A , t2 将 value 的值修改成了 B
时间点3.线程 t2 再次更新 value 的值
线程 t2 再次操作将 value 的值从 B 更改成 A
时间点4. 线程 t1 继续执行 CAS 操作
线程 t1 检查 value 值发现与最开始读到的值相同,执行 CAS ,认为没有被修改过,将 value 的值更新成 C
举个生动点的例子
比如我要去 ATM 机上取钱,取50快,我原来有100快
设置了两个线程,两个线程都是通过CAS操作来执行,都是-50
正常情况下,两个线程都获取到 100
块钱并存入 OldValue
。
• 其中一个线程开始执行 CAS
操作,另一个线程则处于等待状态。
• 先进行的线程发现 OldValue
和当前的 value
相等,于是扣除 50
块钱,操作完成并退出。
• 第二个线程发现 OldValue
和 value
不一样,所以没有执行扣款操作,并返回 false
。
异常情况下,两个线程同样获取到 100
块钱并存入 OldValue
。
• 其中一个线程开始执行 CAS
操作,另一个线程继续等待。
• 先执行的线程发现 OldValue
和 value
相等,扣除 50
块钱。但此时你转了 50
块钱到我的卡 上,使得 value
又变成了 100
。
• 先执行的线程退出后,第二个线程发现 OldValue
和当前的 value
依然相等,于是错误地再次扣除 50
块钱,最终出现了多次扣款的 bug。
CAS只能对比最终值是否相同,无法确定值中间是否发生变化
解决方法
我们可以引入版本号变量,约定版本号只能增加
每次CAS对比的时候就不是数值本身,而是对比版本号
如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败