CAS你必须知道的几点
概述
CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS也是现在面试经常问的问题,本文将深入的介绍CAS的原理。
案例
介绍CAS之前,我们先来看一个例子。
/** * @author dajun * @date 2019/7/6 */ public class VolatileTest { public static volatile int race = 0; private static final int THREADS_COUNT = 20; public static void increase() { race++; } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(race); } }
这个例子有些网友反馈会进入死循环,我后面也发现了,在IDEA的RUN模式下确实会陷入死循环,通过 Thread.currentThread().getThreadGroup().list(); 代码可以打印出当前的线程情况如下:
java.lang.ThreadGroup[name=main,maxpri=10] Thread[main,5,main] Thread[Monitor Ctrl-Break,5,main]
可以看到,除了Main方法线程后,还有一个Monitor Ctrl-Break线程,这个线程是IDEA用来监控Ctrl-Break中断信号的线程。
解决死循环的办法:如果是IDEA,可以使用DEBUG模式运行就可以,或者使用下面这段代码。
import java.util.concurrent.CountDownLatch; /** * @author dajun * @date 2019/7/6 */ public class VolatileTest { public static volatile int race = 0; private static final int THREADS_COUNT = 20; private static CountDownLatch countDownLatch = new CountDownLatch(THREADS_COUNT); public static void increase() { race++; } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } countDownLatch.countDown(); } }); threads[i].start(); } countDownLatch.await(); System.out.println(race); } }
CAS是什么?
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
源码分析
上面源码分析时,提到最后调用了compareAndSwapInt方法,接着继续深入探讨该方法,该方法在Unsafe中对应的源码如下
CAS的缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
- 循环时间长开销很大。
- 只能保证一个变量的原子操作。
- ABA问题。
循环时间长开销很大:
CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
只能保证一个变量的原子操作:
当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。但是我们可以通过以下两种办法来解决:1)使用互斥锁来保证原子性;2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。
什么是ABA问题?ABA问题怎么解决?
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。