CAS介绍与使用
1. 概念与作用
CAS(Compare And Swap),即比较与交换,比较内存和寄存器中的值然后进行交换,举个栗子🌰:
如图所示,CAS需要执行一个操作:
- 将内存中变量的值与寄存器中的预期值(expected)比较
- 若两者的值相等,交换值(swap)与内存中的值,并返回更新成功的结果
- 若两者的值不相等,返回更新失败的结果
工作流程如图所示:
CAS伪代码(下面操作不是原子性的,真正的CAS是一个原子的硬件指令完成的,这里的伪代码只是为了辅助理解CAS的工作流程):
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
上述操作看似分了几个步骤,但CAS的操作都是原子性的,是一个原子操作,相当于只执行了一条CPU指令,这能够使我们在多线程环境下能够达到“无锁化编程”的效果,即不需要加锁而合理使用CAS也能解决因多线程的随机调度带来的线程安全问题(通过原子性解决的根源上的问题),同时也避免了加锁带来的线程阻塞!!
2. CAS的应用
2.1 原子类
java标准库对CAS进行了进一步的封装,给我们提供了一些原子类,能够使用这些类来实现原子性的操作:
这里我们举个例子:
public class Test8 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 50000;i++) {
count++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 50000;i++) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
上述代码存在线程安全问题,因此并不能得到正确的结果,之前我们是通过加锁的方式来解决问题的,但这次我们使用原子类来尝试解决问题:
import java.util.concurrent.atomic.AtomicInteger;
public class Test8 {
// private static int count = 0;
static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 50000;i++) {
count.getAndIncrement();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 50000;i++) {
count.getAndIncrement();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count.get());
}
}
上面我们使用到了一个原子类AtomicInteger
,在上述代码中通过它实现了对一个数进行++操作,通过它执行的操作都是具有原子性的,进而不需要通过解锁也能解决线程安全问题!减少了锁竞争带来的线程阻塞我们的程序执行效率也就提高了!!
这个原子类的伪代码实现如下:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while (CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
工作流程:一个线程对数据进行了更新,value更新,此时有其它线程也准备对数据进行修改,会发现value和oldvalue不一样了,就知晓已经有线程对数据进行了修改,于是将修改后的value数据重新引入
2.2 自旋锁
通过CAS实现自旋锁,能够更灵活的获取到锁:
public class SpinLock {
private Thread owner = null;
public void lock() {
// 通过CAS查看当前锁是否被某个线程持有
// 若owner为null,则代表锁未被其它线程持有,将owner设置为当前尝试加锁的线程
// 若owner不为null,则继续循环尝试获取锁
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
3. ABA问题
CAS通过判断内存中的值与寄存器中的预期值是否一致来辨别当前内存中的数据是否被修改过,因为是原子性操作所以一般不会有什么问题,但也存在这样一个特殊情况:
假设现在有两个线程t1和t2,一个共享变量num,num中的初始值为A
现在t1线程想要通过CAS将num中的数值改为Z,则会执行以下操作:
- 将num中的值(A)读取到oldValue中
- 比较此时内存中的值(A)与寄存器中的值oldValue,若相同则修改A为Z
可在t1执行任务之间,t2线程也对共享变量进行了修改,将他从A该成了B,然后又从B改成了A,而对于t1来说它会认为数值没有变化还是A,没有其它线程对它进行修改,继续执行交换操作,这种将数据修改后又修改回来以此瞒过CAS的过程就是ABA问题
上述描述的特殊过程,可能因为CAS执行速度较快(原子性)而没造成什么影响,但一些情况下可能会造成很大的问题,举个栗子🌰:
小明到银行来取钱,目前它的存款里有100块钱,准备取50块钱,在完成手续后通过点击ATM机的取钱按钮来取钱,可他点了一下之后,发现机器没什么反应,于是又点击了一下,假设此时ATM就创建了两个线程来执行任务,两个线程都是通过CAS来执行操作:
- 线程1判断期望值为100后,会执行-50操作
- 线程2也是判断期望值为100后,会执行-50操作
- 此时线程1先拿到了数据,并判断内存中的值与期望值100相同,于是进行-50操作,此时小明存款为50;
- 此时在执行线程2之前,小明的朋友给小明的账户又存了50块钱,那小明的存款就又变为了100;
- 轮到线程2了,它发现小明的存款为100,与期望值相等,于是就又执行了-50操作
小明的取50操作实现了,账户显示存款剩余50,可小明朋友给小明打的50却“不翼而飞”了。
那么,怎么解决CAS的ABA问题呢?
可以给CAS引入版本号,在每次进行期望值与内存值判断时,也需要进行一次版本号的判断:
- 若判断二值相等,还要进行一次版本号判断,若版本号也相等,则可以进行数值交换,且版本号 + 1(无版本号- 1操作)
- 若而值相等但版本号不相等,则说明数据被人修改过,于是操作失败
这样通过引入版本号就解决了CAS带来的ABA问题了!!
以上便是对CAS的介绍与使用了,如果上述内容对大家有帮助的话请给一个三连支持吧💕( •̀ ω •́ )✧( •̀ ω •́ )✧✨