目录
1.CAS是什么?
CAS:全称compare and swap(比较并交换)
我们假设内存中的原始数据V,旧的预期值A,需要修改的新值B
1.比较A与V是否相等(比较)
2.如果比较相等,将B写入V(交换)
3.返回操作是否成功
在这里最特别的是,上述这个CAS的过程,并非是通过一段代码实现的,而是通过一条CPU指令完成的,所以CAS操作是原子的,就可以在一定程度上回避线程安全问题,所以我们在解决线程安全问题除了加锁之外,又有了一个新思路。
但是我们可以通改CAS伪代码来帮助我们理解这个特殊指令:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue){
&address = swapValue;
return true;
}
return false;
}
2.CAS的应用场景
2.1 实现原子类
Java标准库里面提供的类
AtomicInteger count = new AtomicInteger(0);
count.getAndDecrement();
count.getAndDecrement();相当于i++
伪代码实现
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while (CAS(value, oldValue, oldValue + 1) != true) {
oldValue = value;
}
return oldValue;
}
}
现在假设有两个线程同时调用上面的getAndDecrement(),
2)线程1先执行CAS操作,由于oldValue和value的值相同,直接进行对value的赋值。
此时的value值变成1,但是CAS的返回值为true,所以oldValue的值仍然为0。
2.2 实现自旋锁
实现自旋锁伪代码
public class SpinLock {
private Thread owner = null;
public void lock() {
while (!CAS(this.owner, null, Thread.currentThread())) {
}
}
public void unlock() {
this.owner = null;
}
}
private Thread owner = null;
这个代码的含义是当前锁是谁加的。
public void lock() {
while (!CAS(this.owner, null, Thread.currentThread())) {
}
}
检测当前的owner是否为null,如果为空,就将当前线程赋值给owner。如果赋值成功,则加锁完成,循环结束。如果当前锁已经被其他线程占用,CAS操作会失败,因为this.owner不是null。此时返回false,继续循环进行下一次判定。
3.CAS的典型问题:ABA问题
它的核心思想是检查当前值与预期值是否相等,如果相等则进行交换操作。然而,ABA问题是一个潜在的问题,即在CAS操作过程中,一个变量的值被修改两次后又恢复到了原来的值,导致CAS误判为没有发生变化,这个问题就叫ABA问题。
虽然这个ABA情况,大部分情况下,其实不会对代码/逻辑产生太大影响,但是不排除一些极端情况,也有可能造成影响。举一个极端的例子:
在考虑使用CAS方式扣款的情况下,假设我现在需要取款500元,我的账户余额为1000元。当我按下取款按钮时,机器可能会卡住,如果我不小心多次按下按钮,可能会导致重复扣款的情况发生,如下图
在执行第二次CAS操作时,如果此时有人转账500元进来,账户余额仍然为1000元,CAS操作会继续扣款。
为了解决这个问题,我们可以引入一个版本号或时间戳的概念。每次对变量进行修改时,同时更新版本号或时间戳。在进行CAS操作时,除了比较当前值和预期值之外,还需要比较版本号或时间戳。只有当两者都匹配时,才认为变量没有被中途修改过,可以进行交换操作。
例如,假设我们有一个变量value
和一个对应的版本号version
,初始值为1。当我们想要更新value
时,首先获取当前的version
,然后执行更新操作,并将version
加1。在进行CAS操作时,我们需要同时比较value
和version
。只有当value
和version
都与预期值匹配时,才执行交换操作。
通过引入版本号或时间戳,我们可以解决ABA问题,确保在并发环境下的正确性。