概述
- CAS是Compare And Swap的简称,即:比较并交换
- Java中常见的是Atomic相关类使用了CAS,例如:AtomicInteger、AtomicBoolean等等
- 实现CAS的底层用的是Unsafe操作类
先看个小例子
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5, 100);
System.out.println(atomicInteger.get());
atomicInteger.compareAndSet(5, 200);
System.out.println(atomicInteger.get());
}
输出:
100
100
- compareAndSet(int expect, int update):将atomicInteger数值与expect比较,相等就把update赋给atomicInteger,否则什么都不做。
- 所以,上面的例子只有第一次是修改成功的,第二次修改失败,值仍然为100.
compareAndSet 源码:调用了unsafe.compareAndSwapInt
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
- this:当前AtomicInteger对象
- valueOffset:AtomicInteger的value字段在内存中的偏移量
- expect:期望值,需要比较的值
- update:如果value==expect,需要更新替换(Swap)的数据
Unsafe
- Unsae是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地native方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
- Unsafe类存在于sun.misc包中,其内部方法可以像C语言的指针一样直接操作内存,因此Java中CAS的操作执行依赖于Unsafe类的方法。
现在,再来看看上面的compareAndSet,内部使用的unsafe.compareAndSwapInt,其实底层就一句native代码。但是,最终执行是由一系列原子的、不可穿插、不可中断的指令组成,以此来实现CAS原子性操作
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
例子2:getAndIncrement
AtomicInteger还有个常用的方法:getAndIncrement。作用就是把当前数值+1,跟i++
效果一样。但是getAndIncrement是线程安全的,而i++不是线程安全。
getAndIncrement 源码解析,调用的是 unsafe.getAndAddInt
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
再来看下 unsafe.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;
}
- 根据 AtomicInteger 的 valueOffset,即内存偏移量,获取内存中的值 var5
- 执行 compareAndSwapInt,把 var5 再次和内存的值比较一次,相同就执行 var5 + var4(这里var4=1)
- 如果上一步比较结果不同,则重新回到第1步,重新执行
- 返回 var5
CAS缺点
- getAndAddInt使用的是do…while,执行失败会一直反复尝试。如果长时间一直不成功,会给CPU带来较大的开销。
- 只能保证一个共享变量的原子操作,如果需要保证多个变量的原子操作,可以采用锁,或者AtomicReference操作对象
- 引出来ABA问题
ABA问题
ABA 问题阐述
- 假设主内存有变量A=0,现在2个线程都准备执行compareAndSet(0, 10)
- 由于线程2的执行速度较快,把A改成10,然后又改成了20,最后又改成10
- 此时线程1也正准备执行,因为主内存A=10,线程1中也是A=10,就以为当前变量A没人操作过,所以线程1的compareAndSet也执行成功了
- 表面上看似乎也没什么问题,但是这个现象,在特定的业务条件下可能就会导致Bug
ABA 问题解决
使用AtomicStampedReference可以解决ABA问题,因为这个类携带版本号功能。除了比较当前值和预期值,还会比较当前版本号和预期版本号。如果版本号不一致,则同样更新失败。
public class AtomicStampedReferenceApp {
public static void main(String[] args) {
//初始化AtomicStampedReference对象,值为5,版本号为1
AtomicStampedReference<Integer> atomic = new AtomicStampedReference<Integer>(5, 1);
new Thread(() -> {
int stamp = atomic.getStamp();
System.out.println(Thread.currentThread().getName() + "初始版本号:" + stamp);
//Sleep 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//先从5改成10
atomic.compareAndSet(5, 10, atomic.getStamp(), atomic.getStamp()+1);
//再从10改成5
atomic.compareAndSet(10, 5, atomic.getStamp(), atomic.getStamp()+1);
//输出结果,这时值为5,版本号为3
System.out.println(Thread.currentThread().getName() + "当前值:" + atomic.getReference() + ",当前版本号:" + atomic.getStamp());
}, "thread-1").start();
new Thread(() -> {
int stamp = atomic.getStamp();
System.out.println(Thread.currentThread().getName() + "初始版本号:" + stamp);
//Sleep 3s,保证thread-1先执行完毕,到时版本号会更高
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//预期将值从5改成100,预期版本号是1
boolean result = atomic.compareAndSet(5, 100, stamp, stamp+1);
System.out.println(Thread.currentThread().getName() + "修改成功:" + result + ",当前值:" + atomic.getReference() + ",当前版本号:" + atomic.getStamp());
}, "thread-2").start();
}
}
结果如下,thread-2修改失败,因为当前版本号已经变成3了,比预期的1还高
thread-1初始版本号:1
thread-2初始版本号:1
thread-1当前值:5,当前版本号:3
thread-2修改成功:false,当前值:5,当前版本号:3