CAS的概念(compare and swap):比较并交换
我们首先要明白,自旋是一个纯用户态的操作,本身并没有进行加锁,所执行的代码,是在并发环境下执行的,没有锁。
我们举一个例子来说明自旋加CAS的作用:
我们画一个时间图来接着证明:
线程中的Load和CAS的load并不一样,线程中的Load是获取预期值,而CAS中的load是在主内存中获取值,与预期值进行比较,两者作用并不相同。我们发现CAS并不是一个操作,首先要进行预期值和主内存中的值的比较,如果相同再进行下一步的操作,不相同则重新自旋+CAS,伪代码如下,按理说应该对应着许多条cpu指令,但是事实上它只对应着一条cpu指令(cmpxchg),一条指令可以完成所有操作,这条指令对应着系统的一个API,这个API有三个参数:地址值,预期值和要修改的值,但是我们必须要明白的是CAS的实现是通过cpu硬件提供的lock机制保证其原子性,简而言之就是因为硬件层面给予了支持,软件层面才能达到。
注意:CAS最重要的是判断我们第一次获取到的预期值和主内存中的值是否一致,也就是说数据是否在我们获取完预期值之后被其他线程修改过,没有修改过我们在此基础上进行的修改和写回操作是线程安全的,如果被修改过,此时我们再进行的修改和写回操作可能覆盖之前修改的值,这是线程不安全的。
CAS的两个应用:
①实现自旋锁:
伪代码如下:
②实现原子类
我们关于原子类的代码如下:
public class demo1001 {
public static void main(String[] args) throws InterruptedException {
//创建基于CAS的原子类
AtomicInteger atomicInteger = new AtomicInteger();
//创建两个线程实现自增操作
Thread t1 =new Thread(()->{
for (int i = 0; i <50000 ; i++) {
atomicInteger.getAndIncrement();
}
});
Thread t2 =new Thread(()->{
for (int i = 0; i <50000 ; i++) {
atomicInteger.getAndIncrement();
}
});
//两个线程均开始执行
t1.start();
t2.start();
//等待两个线程均执行结束
t1.join();
t2.join();
System.out.println(atomicInteger);
}
}
在这段代码中,我们并没有使用synchronized进行上锁,但是结果仍然是正确的
我们查看其源码:
关于CAS中的ABA问题:
ABA 的问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
先读取 num 的值, 记录到 oldNum 变量中.
使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这个时候 t1是否要更新值为Z呢?
那在实际生活中我们举这样一个例子:
虽然从值的大小来看虽然是一样的,但是从操作过程和逻辑上来说有着很大的区别,可以说是两个完全不同的结果,那么我们又该如何解决这个ABA问题呢?
答案很简单,增加一个版本号,当有人对这个数据操作之后,该数据的版本号加1,虽然结果一致,但是我们可以通过版本号来判断这个结果是否是我们期望的结果