面试||AtomicInteger原子类实现CAS
CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止
CAS应用
CAS有三个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做,进行自旋
我们先来一个demo感觉一下:
/**
* CAS是什么?========>compareandset
* 比较并交换
* 和期望值比较为true则修改
* 和期望值比较为false则不修改
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019)+"\t current data"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,1024)+"\t current data"+atomicInteger.get());
}
}
最后的输出结果:
true current data2019
false current data2019
从这个简单的demo可以得出:
先给atomicInteger赋值为5,使主内存中的值为5,第一次与期望值5进行比较,如果正确则将主内存中的值改为201,第二条system语句中又与另外一个期望值5比较,此时主内存中的已经被更新为2019,所以相比较不同,返回false,主内存中的值2019页不会被改为1024
我们开始从原子类AtomicInteger这个类中的自增加一这个函数来了解CAS,当然也有其他的函数,自减,自增N等
这个自增加一的函数实际上是调用的是unsafe这个对象的getAndAddInt(this, valueOffset, 1)这个方法,传入参数
this:当前对象,也就是AtomicInteger对象本身
valueOfset:内存偏移量(内存地址)
1 就是自增的值为1,我们跟下去:
下面是getAndAddInt方法:
参数(三个参数刚也介绍了,传了过来,我们再介绍一下):
var1:AtomicInteger对象本身
var2:就是对象值的引用地址
var4:需要变动的数量(这里是1)
var5:是自己临时定义的一个变量,通过var1和var2找出主内存的值,用它来存储并返回主内存的值
用该对象当前的值与var5比较
如果相同,更新var5+var4,并且返回true
如果不同,继续取值然后再比较,直到更新完成
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
这是compareAndSwapInt这个方法,其实是一个本地方法,进行数值比较并交换,是CPU原语,实现在unsafe.cpp中
CAS缺点:
1、循环时间长,开销大
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
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;
}
通过不断自旋判断,在多线程的模式下花费大量时间
2、只能保证一个共享变量原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁保证原子性
3、ABA问题
CAS会导致“ABA问题”
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化
比如说一个线程one从内存位置V中取出A,这时候另外一个线程two也从主内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成为A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的
解决CAS问题
通过带有时间戳的原子引用类解决CAS问题,虽然你修改一次,然后又修改了回来,可是修改版本的次数一直在增加,这样就确保线程睡眠或挂起时,另外一个线程到底有没有修改值
t1、t2线程为考究原子引用类
t3、t4线程为考究带有时间戳的原子引用类,解决CAS问题
public class ABADemo {
static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
//保证上面的t1线程完成一次ABA操作
try{
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100,2019)+"\t"+atomicReference.get());
},"t2").start();
try{
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("===================解决ABA问题=====================");
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号"+stamp);
try{
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp());
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);
//暂停三秒,保证上面的t3线程完成一次ABA操作
try{
TimeUnit.SECONDS.sleep(3);
}catch (Exception 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();
}
}