谈一谈对CAS的理解
题目描述: 提问线路CAS—> Unsafe—> CAS底层原理 —> 原子 引用更新 —> 如何规避ABA问题
1. 面试题分析
- compareAndSet怎么用?
a. 比较并交换(compareAndSet)
/**
* boolean compareAndSet(int expect, int update)
* * - 如果主内存的值=期待值expect,就将主内 存值改为update
* * - 该方法可以检测线程a的操作变量X没有被其 他线程修改过
* * - 保证了线程安全
*/
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.comp areAndSet(5, 10) + "\t" + atomicInteger);
System.out.println(atomicInteger.comp areAndSet(5, 20) + "\t" + atomicInteger);
//true 10
// false 10
}
2. CAS底层原理简述?
- Compare-And-Swap。是一条CPU并发原语。(原语: 操作系统范畴,依赖硬件,不被中断。)
- 功能是判断内存某个位置的值是否为预期值 (Compare),是就更新(Swap),这个过程是原子 的。
- 功能描述:
a. 判断内存某个位置的值是否为预期值 (Compare),是就更新(Swap),这个过程 是原子的。
b. cas有三个操作数,内存值V,旧预期值A,要更 新的值B。仅当预期值A=内存值V时,才将内存 值V修改为B,否则什么都不做。 - 自旋:比较并交换,直到比较成功
- 底层靠Unsafe类保证原子性。
- getAndIncrement() 源码解析(用了cas保证线程安 全)
// 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;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* 参数说明:
* * this: atomicInteger对象
* * valueOffset:对象的内存地址
* * unsafe:sun.misc.Unsafe类
* * AtomicInteger中变量value使用volatile 修饰,保证内存可见。
* * 结论:底层依赖CAS操作/Unsafe类
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* compareAndSwapInt:即CAS
* * while: 如果修改失败,会一直尝试修改,直到 成功。
*/
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;
}
简述:
i. 首先value必须使用了volatile修饰,这就保证了他的可见性与有序性
ii. 需要初始化value的偏移量
iii. 调用了Unsafe类的getAndAddInt
iv. getAndAddInt使用cas一直循环尝试修改主内存(通过偏移量进行CAS操作,每次从内存中读取数据然后将数据进行+1操作, 然后对原数据,+1后的结果进行CAS操作,成功的话返回结果,否则重试直到成功为止。)
3. 对Unsafe的理解?
Unsafe类
a. 该类所有方法都是native修饰,直接调用底层资 源。sun.misc包中。
b. 可以像C的指针一样直接操作内存。java的CAS 操作依赖Unsafe类的方法。
4. CAS有哪些缺点?
- 循环时间长,开销大
a. 如果cas失败,就一直do while尝试。如果长时 间不成功,可能给CPU带来很大开销。 - 只能保证一个共享变量的原子操作
a. 如果时多个共享变量,cas无法保证原子性,只 能加锁,锁住代码段。 - 存在ABA问题。
5. 拓展内容
- ABA问题
- 简述ABA问题和解决方案?
a. ABA问题描述: 线程1做CAS操作将A改为B再改 为A,而线程2再做CAS时修改成功了,这不符 合设计思想。
怎么解决:AtomicStampReference时间戳原子 引用 - ABA问题描述?问题出在哪?
a. ABA问题描述: 比如线程1从内存位置V中取出A,此时 线程2也取出A。且线程2做了一次cas将 值改为了B,然后又做了一次cas将值改 回了A。此时线程1做cas发现内存中还是 A,则线程1操作成功。这个时候实际上 A值已经被其他线程改变过,这与设计思 想是不符合的。
这个过程问题出在哪?
如果只在乎结果,ABA不介意B的存在, 没什么问题
如果B的存在会造成影响,需要通过 AtomicStampReference,加时间戳解 决。 - 原子更新引用是啥?
a. AtomicStampReference,使用时间戳,解决cas 中出现的ABA问题。
AtomicReference使用代码演示 demo
/**
* 如果希望原子操作的变量是 User,Book,此时需要使用 AtomicReference类
*/
public static void main(String[] args) {
User z3 = new User("z3", 18);
User l4 = new User("l4", 19);
AtomicReference atomicReference = new AtomicReference<>(z3);
System.out.println(atomicRefere nce.compareAndSet(z3, l4)
+ "\t" + atomicReference.get().toString());
System.out.println(atomicRefere nce.compareAndSet(z3, l4)
+ "\t" + atomicReference.get().toString());
//truecom.mxx.juc.User@4554617c
// false com.mxx.juc.User@4554617c
}
AtomicReference存在ABA问题代 码验证
AtomicReference atomicReference = new AtomicReference(100);
/**
* ABA问题验证: * 1--ABA * 2--A,C * @param args
*/
public static void main(String[] args) {
ABADemo abaDemo = new ABADemo();
new Thread(() -> {
abaDemo.atomicReference.compare AndSet (100, 101);
abaDemo.atomicReference.compare AndSet (101, 100);
}, "1").start();
new Thread(() -> {
// 睡1s等线程1执行完ABA
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(abaDemo.atom icReference.compareAndSet(100, 2020)
+ "\t" + abaDemo.atomicReferen ce.get());
//true 2020
}, "2").start();
}
AtomicStampReference解决ABA 问题代码验证
解决思路:每次变量更新的时候,把变量的版 本号加一,这样只要变量被某一个线程修改 过,该变量版本号就会发生递增操作,从而解 决了ABA变化
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100, 1);
public static void main(String[] args) {
// ABAProblem();
ABADemo abaDemo = new ABADemo();
new Thread(() -> {
// 等线程2读到初始版本号的值
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1 在ABA前的版本 号:" + abaDemo.atomicStampedRefere nce.getStamp());
abaDemo.atomicStampedReference.compareAndSet(100, 101,
abaDemo.atomicStampedReference.getStamp(),
abaDemo.atomicStampedReference.g etStamp() + 1);
abaDemo.atomicStampedReference.compareAndSet(101, 100,
abaDemo.at omicStampedReference.getStamp(), abaDemo.atomicStampedReference.g etStamp() + 1);
System.out.println("线程1 在ABA后的版本 号:" + abaDemo.atomicStampedRefere nce.getStamp());
}, "1").start();
new Thread(() -> {
// 存一下修改前的版本号
int stamp = abaDemo.atomicStampedReference.g etStamp();
System.out.println("线程2 在修改操作前的版本号:" + stamp);
// 睡1s等线程1执行完ABA
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(abaDemo.atomicStampedReference.compareAndSet(100, 2020,
stamp, abaDemo.atomicSt ampedReference.getStamp() + 1)
+ "\t" + abaDemo.atomicStampedReference.g etReference());
//线程2在修改操作前的版本 号:1
// 线程1在ABA前的版本号:1
// 线程1在ABA后的版本号:3
// false 100
}, "2").start();
}