面试系列----谈谈对CAS的理解
0.什么是CAS
CAS就是compareAndSet,即比较并交换,是一种实现并发算法时常用到的技术。 先看下面的代码
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,10)+"\t"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,110)+"\t"+atomicInteger.get());
}
}
运行结果:
true 10
false 10
这就CAS比较并交换,就是compareAndSet有两个参数一个是期望值一个是更新值,当内存中的数值是期望值是就修改为更新值,若内存中的值不是期望值的时候,就不会更新,并返回False。
1.CAS的原理
在并发条件下,AtomicInteger类中的getAndIncrement()可以保证原子性,其底层代码为
// 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;
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
使用的是Unsafe类的getAndAddInt方法
1.UnSafe类是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后面,基于该类可以直接操作特额定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UNSafe类的方法。注意UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
2.变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的
3.变量value和volatile修饰,保证了多线程之间的可见性.
CAS的全称为Compare-And-Swap ,它是一条CPU并发原语.它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的.
CAS并发原语提现在Java语言中就是sun.misc.UnSaffe类中的各个方法.调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令.这是一种完全依赖于硬件功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的
,在执行过程中不允许中断,也即是说CAS是一条原子指令
,不会造成所谓的数据不一致的问题。
其中getAndAddInt的源码如下:
/**
var1 AtomicInteger对象本身.
var2 该对象值的引用地址
var4 需要变动的数值
var5 是用过var1 var2找出内存中的值
用该对象当前的值与var5比较
如果相同,更新var5的值并且返回true
如果不同,继续取值然后比较,直到更新完成
**/
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;
}
这里就体现了CAS的原理。
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):
1.AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.
2.线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起.
3.线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK.
4.这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.
5.线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功.
CAS其实就是比较当前工作内存和主内存中的值,如果相同则执行规定的操作,否则继续比较直到主内存和工作内存中的值保持一致为止。
2.CAS的缺点
1.循环时间长开销大,我们可以看到getAndAddInt方法中有一个dowhile循环,如果CAS一直失败,会一直保持尝试。如果CAS长时间一直不成功,可能给CPU带来很大的开销。
2.只能保证一个共享变量的原子性。当对一个共享变量执行操作时,我们可以使用循环CAS的方式保证原子性操作,但是对多个共享变量的操作时,循环CAS就无法保证操作的原子性了,这个时候可以用锁来保证原子性。
3.会出现ABA问题
什么是ABA问题
CAS算法实现的一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并交换,那么这个时间差会导致数据的变化。比如一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two进行了一些操作后将值变成了B,然后线程two又将位置V的数据变成A,这时候线程one进行CAS操作发现内存中仍为A,然后线程one操作成功。尽管线程one的CAS操作成功,但不代表这个过程没有问题。
如何解决ABA问题
使用时间戳原子引用来解决这个问题。简单的说就是在修改数值的时候带上一个版本号,比如一个线程one从内存位置V中取出A并带有版本号1,这个时候另一个线程two也从内存中取出A也带着版本号1,并且线程two进行了一些操作后将值变成了B则将版本号加一变成2,然后线程two又将位置V的数据变成A同时将版本号加一变成3,这时候线程one进行CAS操作发现内存中A但是版本号并不是1了,则显示操作失败。
在原子类中有一个AtomicStampedReference类可以解决带有版本号解决这个问题。
下面代码展示ABA问题与ABA问题的解决
public class ABADemo {
/**
* 这里的Integer不能大于127若大于127则不会出现ABA情况,也就是没有CAS
* true 128
* false 128
*/
private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("===以下是ABA问题的产生===");
new Thread(() ->{
System.out.println(atomicReference.compareAndSet(100, 127)+"\t"+atomicReference.get());
System.out.println(atomicReference.compareAndSet(127, 100)+"\t"+atomicReference.get());
},"t1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019)+"\t"+atomicReference.get());
},"t2").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===以下是ABA问题的解决===");
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(100, 127,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1)+"\t"+atomicStampedReference.getReference());
System.out.println(Thread.currentThread().getName()+"\t第二次版本号:"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(127, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1)+"\t"+atomicStampedReference.getReference());
System.out.println(Thread.currentThread().getName()+"\t第三次版本号:"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,stamp+1)+"\t"+atomicStampedReference.getReference());
System.out.println(Thread.currentThread().getName()+"\t最新的版本号:"+atomicStampedReference.getStamp());
},"t4").start();
}
}