什么是CAS
CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用。
简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。
CAS的优缺点
优点:
- 由于CAS是非阻塞的,可避免死锁,线程间的互相影响非常小。
- 没有锁竞争带来的系统开销,也没有线程间频繁调度的开销。
缺点:
- 可能自旋循环时间过长。如果某个线程通过CAS方式操作某个变量不成功,长时间自旋,则会对CPU带来较大开销。怎么解决:限制自旋次数。
- 只是一个变量的原子性操作,不能保证代码块的原子性。
- ABA问题。
CAS底层原理
- java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
- unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
- Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性
CAS 可能会导致什么问题?
CAS存在ABA问题:
-
当前线程只能感知共享变量的值有没有改变,假设别人从A改成了B,又改成了A,则当前线程感知不到,即ABA问题
-
可以使用版本号机制解决ABA问题,如原子类AtomicStampedReference,加入了版本号,每当修改值,版本号就会+1
-
如 AtomicStampedReference 加入了版本号,每当修改值,版本号就会+1。这样根据版本号就知道有没有别的线程修改过,而且还能知道修改过几次
-
再如 AtomicMarkableReference,他的版本号是一个boolean类型,所以只能判断是否修改过值,不知道修改过几次。
AtomicMarkableReference只能缓解ABA问题,如果别人修改标记后,又改回来,则无法感知。
CAS 机制可以高效地实现原子操作,但仍不完美:
- 循环时间长开销大:CAS 大量失败后长时间占用 CPU 资源,加大了系统性能开销
- 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性
- ABA 问题:CAS 机制本质上依赖值有没有发生变化的操作条件。但是如果值原来是 A、被改成变成了 B、最后又变回了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化进行了操作,但是实际上却变化了,这其实违背了约定的条件。
Java的JUC包里有没有现成的类可以解决CAS的ABA问题?
- 原子引用:(乐观锁思想) AtomicStampedReference 类可以解决ABA问题。这个类维护了一个【版本号】Stamp,其实有点类似乐观锁的意思。在进行 CAS 操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。
/*** @description: 解决 CAS 带来的 ABA问题*/
public class ABADemo2 {//6为传入的值,1为版本号static AtomicStampedReference<Integer> atomic = new AtomicStampedReference<>(6, 1);public static void main(String[] args) {//使用版本号机制来验证ABA问题new Thread(() -> {//获取当前版本号int stamp = atomic.getStamp();System.out.println("线程A1的版本号为:" + stamp);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存//compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp);atomic.compareAndSet(6, 10, atomic.getStamp(), atomic.getStamp() + 1);System.out.println("线程A2的版本号为:" + atomic.getStamp());System.out.println(atomic.compareAndSet(10, 6, atomic.getStamp(), atomic.getStamp() + 1));System.out.println("线程A3的版本号为:" + atomic.getStamp());}, "线程A:").start();//使用【乐观锁】思想解决ABA问题new Thread(() -> {int stamp = atomic.getStamp();System.out.println("线程B1的版本号为:" + stamp);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(atomic.compareAndSet(6, 2, stamp, stamp + 1));System.out.println("线程B2的版本号为:" + atomic.getStamp());}, "线程B:").start();}
}
- AtomicStampedReference<>(6, 1); 这两个参数不要填的太大,因为我们本次测试用的是 Integer 类型
- Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new,因为 valueOf 使用缓存,而 new一定会创建新的对象分配新的内存空间。
为什么会导致ABA问题?
这是因为 CAS 算法是在某一时刻取出内存值然后在当前的时刻进行比较,中间存在一个时间差,在这个时间差里就可能会产生 ABA 问题。
Java哪些地方使用了CAS?
Java提供的API中使用CAS的地方有很多,比较典型的使用场景有原子类、AQS、并发容器。
- 对于原子类,以AtomicInteger为例,它的内部提供了诸多原子操作的方法。如原子替换整数值、增加指定的值、加1,这些方法的底层便是采用操作系统提供的CAS原子指令来实现的。
- 对于AQS,在向同步队列的尾部追加节点时,它首先会以CAS的方式尝试一次,如果失败则进入自旋状态(在队列中自旋),并反复以CAS的方式进行尝试。此外,在以共享方式释放同步状态时,它也是以CAS方式对同步状态进行修改的。
- 对于并发容器,以ConcurrentHashMap为例,它的内部多次使用了CAS操作。
- 在初始化数组时,它会以CAS的方式修改初始化状态,避免多个线程同时进行初始化。
- 在执行put方法初始化头节点时,它会以CAS的方式将初始化好的头节点设置到指定槽的首位,避免多个线程同时设置头节点。在数组扩容时,每个线程会以CAS方式修改任务序列号来争抢扩容任务,避免和其他线程产生冲突。
- 在执行get方法时,它会以CAS的方式获取头指定槽的头节点,避免其他线程同时对头节点做出修改。
CAS的实现离不开操作系统原子指令的支持,Java中对原子指令封装的方法集中在Unsafe类中,包括:原子替换引用类型、原子替换int型整数、原子替换long型整数。这些方法都有四个参数:var1、var2、var4、var5,其中var1代表要操作的对象,var2代表要替换的成员变量,var4代表期望的值,var5代表更新的值。