CAS
什么是CAS
compare and swap
比较并交换
把内存中的某个值,和CPU寄存器中的某个值,进行比较~
如果两个值相同,就把另一个寄存器中的值和内存的值进行交换(把内存的值放到寄存器B,同时把寄存器B的值写给内存)
CAS伪代码:
下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解CAS 的工作流程
//address 内存的地址, expectValue 寄存器A的值, swapValue 寄存器B的值
boolean CAS(address, expectValue, swapValue) {
//判断address处放的数据和expectValue是否相同,如果相同,就把寄存器B的值写到内存中
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
这里的赋值其实是"交换",不关心寄存器B里是啥,更关心内存中是什么,把交换近似的堪称赋值~
上述这一组操作,是通过一个CPU指令完成的!!
上述操作是原子的!!是线程安全的!!同时还高效!!并且还不涉及锁冲突以及线程等待!
-
基于
CAS
就可以在实现一些逻辑的时候,不加锁也能保证线程安全 -
CAS
是硬件实现的逻辑~ -
CAS
看起来比加锁更好,但是CAS
只是在特定场景能用,加锁适用面更广,加锁代码往往比CAS
可读性更好~
CAS最常用的两个场景:
- 实现原子类
- 实现自旋锁
实现原子类
比如多线程count++
这个功能,多线程环境下,是线程不安全的,如果想要线程安全,就需要加锁,加锁性能就大打折扣…
这种功能就可以基于CAS操作来实现"原子"的 ++
操作,从而保证线程安全 & 高效
伪代码实现:
//其实是标准库里已经封装好的一个类
class AtomicInteger {
private int value;
//getAndIncrement()方法就相当于count++
public int getAndIncrement() {
//此处的oldValue相当于寄存器A,是把内存value的值读到寄存器里
int oldValue = value;
//此处的oldValue + 1 也把它理解成是另外一个寄存器B的值
// 比较看value这个内存中的值,是否和寄存器A的值相同
//如果相同,就把寄存器B的值,给设置到value中
//同时CAS返回true,结束循环
//如果不相同,就继续循环,重新读取内存value的值到寄存器A中
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
图解:
t1
第一次LOAD
,把value
的值放到t1
的寄存器中
t1
LOAD
完成后,按照顺序,t2
LOAD
,把value
的值放到t2
的寄存器中
t2
LOAD
完成后,t1
进行CAS
,t1
中寄存器的值跟value
比较,二者值相同,就把另外一个寄存器中0 ++
的值赋给value
t2
寄存器的值和内存中的值进行比较,发现不一样,就进入循环,重新LOAD
t2
CAS
发现数据相同,把另外一个寄存器里准备的2
放到value
中
实现自旋锁
自旋锁是纯用户态的轻量级锁
当发现锁被其他线程持有的时候,另外的线程不会挂起等待,而是会反复询问,看当前的锁是否被释放了~
伪代码实现:
public class SpinLock {
//owner表示当前这把锁是谁获取到的
//null表示锁是无人获取的状态
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
//比较owner和null是否相同(是否是解锁的状态),如果是,就要进行交换
//把当前调用的lock的线程的值,设置到owner里(相当于加锁成功),同时结束循环
//如果owner不为null,则CAS不进行交换,返回false,会进入循环,又会立即的再次发起判定
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
自旋锁这种实现,属于消耗CPU资源,但是换来的是第一时间获取到锁!
这种操作在有些情境下也是值得使用的~
如果当前预期锁竞争不太激烈的时候(预期在短时间内获取到锁),使用自旋锁就非常合适了~
自旋锁是一个轻量级锁,也是一个乐观锁~
CAS的ABA问题
CAS的一个小缺陷~
举个例子:
我去买个手机,我无法判定这个手机是一个 新机,还是翻新机(二手)
在CAS中,进行比较的时候,发现寄存器A和内存M的值相同
那我无法判定是M始终没变,还是M变了,又变回来了…
一般情况下不会有bug,但是在极端情况下,会出现问题的!
举个ABA在极端情况下的例子:
假设我去超市买零食,零食总价50元,然后扫码付款的时候,卡了下,我就多摁了几下,扫码机器就创建出了两个线程来进行扣款操作,并且扣款是基于CAS来完成:
这是正常的执行方式,没有触发ABS
:
但是如果此时妈妈给我的支付宝转账50,我的余额就会重新变为100,此时就会触发ABS
:
这就导致了一次付款,重复扣款的bug
!!
💗解决方案:
只要有一个记录,能够记录上 内存 中数据的变化,就可以解决ABA问题了~
💫如何进行记录呢?
另外搞一个内存,保存 M 的 “修改次数” (只增不减)或者是 “上次修改时间”(只增不减),此时修改操作就不是把账户余额读取到 寄存器A 中了,比较的时候也不是比较账户余额了 , 而是比较 版本号/上次修改时间 :