CAS
什么是CAS
compareandswap:比较并更新
实现:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止
CAS能做什么
- 解决多线程安全问题:执行操作的时候会进行CAS操作,当发现当前值和预期值相符,会进行下一步操作,当不相符,此次操作失败
CAS在Java中的应用:Atomic系列就是使用CAS实现的
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
CAS的缺点:ABA问题 ;长时间自旋转消耗资源
ABA问题
例子:
- 两个线程1,2进行自增操作,线程1,线程2同时获取当前的值A,
- 线程1比较快,在线程2操作前,进行了两次操作,第一次将A改为B,第二次将B又改为A
- 线程2操作的时候,发现当前值是A,符号预测,就会进行操作
- 长时间自旋
如果一个线程每次获取的值都被其他线程修改,那就会一直进行自旋,直到成功为止,这样会十分消耗资源
ABA的安全隐患
一般情况下ABA并不会出现什么问题,但是涉及引用的时候就回出现问题
例子:用链表实现一个栈,初始化向栈中压入B、A两个元素,栈顶执行A元素
- 线程1想将栈顶换成B,但他获取栈顶A之后,被线程2打断
- 线程2将A B弹出,然后压入C D
- 线程1继续执行,比较A相同,栈顶指向B,但是B已经被弹出
ABA问题的解决
- 加锁
- 原子引用
原子引用:带版本号的原子操作
JUC 包提供了一个带有时间戳的原子引用类 AtomicStampedReference 来解决该问题,它通过控制变量的版本来保证 CAS 的正确性。
public class CASDemo {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(2020, 1);
new Thread(()->{
// 获得版本号
int stamp = atomicInteger.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 后两个参数:期望的版本号
atomicInteger.compareAndSet(0,2,
atomicInteger.getStamp(),atomicInteger.getStamp()+1);
atomicInteger.compareAndSet(2,0,
atomicInteger.getStamp(),atomicInteger.getStamp()+1);
},"线程1").start();
new Thread(()->{
// 获得版本号
int stamp = atomicInteger.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.compareAndSet(0, 6,
stamp, stamp+1);
},"线程2").start();
}
}