CAS和ABA问题
1 CAS
CAS 的全称是 Compare-And-Swap,它是 CPU 并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
实例
底层原理
- 自旋锁
- UnSafe(来自于:
rt.jat/sun/misc/Unsafe.class
):操作系统底层方法的类,原子性由CPU原语保证,getAndIncrement()
方法的底层源码:
能够看到,atomicInteger.getAndIncrement()
方法调用了 unsafe 类的 getAndAddInt()
方法;Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe 类存在sun.misc 包中,其内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中的 CAS 操作的执行依赖于 Unsafe 类的方法。
其中变量 valueOffset
表示该变量值在内存中的偏移地址(即内存地址),因为 Unsafe 就是根据内存偏移地址获取数据的。
简单小结:
- 比较当前工作内存中的值和主内存中的值,如果相同执行规定操作,否则继续比较直到主内存和工作内存的值一致为止。
- CAS 有3个操作数,内存值V,旧的预期值A,要修改更新值B,当且仅当预期值A和内存值V相同时,将内存值修改为B,否则什么都不做。
CAS 缺点
- 循环时间长,开销大
- 只能保证一个共享变量的操作
- 引出ABA问题
2 ABA 问题
从 AtomicInteger 引出下面的问题:
CAS -> Unsafe -> CAS 底层思想 -> ABA -> 原子引用更新 -> 如何规避 ABA 问题
可以理解为 狸猫换太子。就是t1和t2两个线程同时操作主内存中的A时,t1、t2分别将A拷贝到自己的工作内存进行操作,其中t2线程完成较快,它将A改成B,后又将B改回A;当t1线程写回时,发现预期值是A,所以将操作后的结果写回。最后结果看似正常,其实过程中存在着很大的问题。
原子引用
原子引用其实和原子包装类是差不多的概念,就是将一个 java 类,用原子引用类进行包装起来,那么这个类就具备了原子性 。
解决 ABA 问题
原子引用 + 版本号(时间戳):根据版本号判断当前数据是否经过修改。
public class ABADemo {
/**
* 普通的原子引用包装类
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 传递两个值,一个是初始值,一个是初始版本号
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("============以下是ABA问题的产生==========");
new Thread(() -> {
// 把100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
System.out.println("============以下是ABA问题的解决==========");
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t3一秒钟
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 传入4个值,期望值,更新值,期望版本号,更新版本号
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
}, "t4").start();
}
}
我们能够发现,线程 t3,在进行 ABA 操作后,版本号变更成了 3,而线程 t4 在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样 。
根据 Java面试_大厂高频面试题_阳哥 整理