在介绍CAS之前,我们先看一段代码
@Slf4j
public class CASTest {
public static volatile int race = 0;
public static final int THREAD_COUNT = 20;
public static void increase(){
race++;
}
//20个线程
private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT,new ThreadNameFactory());
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < THREAD_COUNT; i++) {
log.debug("第{}个线程",i);
//20 * 10,000 = 200,000 输出结果不为200000
executorService.execute(new ThreadIncrease());
}
Thread.sleep(10000);
System.out.println(race);
}
//线程工厂,用来定义线程名称
static class ThreadNameFactory implements ThreadFactory{
private static final AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r,atomicInteger.getAndIncrement()+"");
}
}
//线程执行方法体
static class ThreadIncrease implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
log.debug("第{}个线程运行结束",Thread.currentThread().getName());
}
}
}
这段代码,无论怎么运行,结果多不会是20000(可能某一次误打误撞会是),这是为什么?
我们知道,volatile关键字是一个轻量级的同步框架,在运行时时,使用volatile修饰的变量的改变对其他线程是可见的。
但是volatile只保证了可见性,没有保证原子性,所以答案就不会是200000。
想让这段代码运行成功很简单,在increase()
方法上加sychnorized
,但是这样性能会大大降低。
那该怎么做呢?使用原子操作类的变量就可了,以下是更改后的代码:
public static AtomicInteger race = new AtomicInteger(0);
public static void increase(){
race.getAndIncrement();
}
再次运行,这次结果就是200000了。
那么原子操作类是怎么保证运行的呢?我们先看getAndIncrement()
的源码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
我们接着往下看,看getAndAddInt
这个方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
在这段代码中,有用的其实就是这个方法compareAndSwapInt()
,他就是我们要介绍的CAS了
compareAndSwapInt()
方法解析:
拿到内存位置的新值var5,使用CAS尝试修改为var5+var4,如果失败,获取新值var5,然后继续,直到成功
CAS是CompareAndSwap的缩写,比较并替换。CAS底层有三个操作数:
CAS在运行时,当且仅当内存地址的V的值与预期值A相等,才会将内存地址对应的V值改为B,具体流程如下:
在上述过程中,程1重新获取内存地址V值,并重新计算要修改的值的过程称为自旋
从思想上来说,CAS属于乐观锁,认为程序并发情况没那么严重,所以会尝试不断更新,而synchronized被称为悲观锁
CAS虽然高效的解决了原子操作问题,但是CAS仍然存在三个大问题:
循环时间长,开销大
只能保证一个共享变量的原子操作
ABA问题:
CAS的过程如下:
但是在第一步中读取的是A,并且在第三步修改成功,但是第二步呢?可不可能被其他线程修改过?答案是肯定的,如果这期间被第二个线程修改过,他的值被改为了B,后来又经过又被改为A,那么CAS就会认为他从来没被改变过(其实目的还是为了改成A),所以继续又改成了B(显然不是我们想要的),这个问题就被称为ABA问题。
Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。