CAS(Compare And Swap)是用于多线程同步的原子性指令,CAS主要通过三个值来实现,原始内存的值,给定的预期值,给定更新值,只有当原始内存的值与给定的预期值相等的情况下,才将更新值返回。
假设有两个线程A,B同时执行a=a+1
根据JMM内存模型,A,B线程分别拷贝一份副本到自己的内存中,假设此时A线程挂起,线程B对a的值进行修改,线程B首先会判断拷贝的副本值是否与共享内存的值相等,如果相等,对a进行更新,并写回主内存,此时线程A要修改a了,但是线程A发现拷贝的副本值与共享内存中的值不同,线程A会重新从共享内存中拷贝副本进行修改。
底层是通过sun.misc.Unsafe+CAS思想来实现的
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe中的类都是native方法,它可以通过valueOffset拿到在内存中的值,然后利用一个do…while循环,判断内存中的值是否与当前的相等,如果相等则对值进行更新,并将更新后的值返回,否则,进入循环中,重新获取内存值的最新值。
public final int getAndAddInt(object o,long offset,int delta){
int v;
do{
v = getIntVolatile(o,offset);//拿到内存位置的最新值
}while(!compareAndSwapInt(o.offset,v,v+delta))//o.offset的值 == v
}
假设有两个线程t1,t2一个共享变量A,线程t1,t2分别将A拷贝一份到自己的内存,这时t1先将A的值改为B
,再将B的值改为A,这时线程T2要修改值,发现符合CAS,对A的值进行更新。这就是ABA问题,线程t1对A的值进行过修改,线程t2完全忽略了这个过程。
AtomicReference ar = new AtomicReference(100);
// 线程t1
new Thread(()->{
ar.compareAndSet(100, 101);
ar.compareAndSet(101,100);
},"t1").start();
// 线程t2
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("是否修改成功:"+ar.compareAndSet(100, 101)+"\t当前最新值为"+ar.get());
},"t2").start();
打印结果 :
是否修改成功:true 当前最新值为101
解决方法
使用原子引用+版本号控制
// 初始值为 100,stamp为 1
AtomicStampedReference asr = new AtomicStampedReference(100, 1);
// 线程t1执行ABA操作,版本更新
new Thread(()->{
int stamp = asr.getStamp();
System.out.println("当前线程"+Thread.currentThread().getName()+"的版本号"+stamp);
try {
//等待线程t2拿到版本号
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+"当前版本号"+asr.getStamp());
asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+"当前版本号"+asr.getStamp());
},"t1").start();
// 线程t2修改值
new Thread(()->{
int stamp = asr.getStamp();
System.out.println("当前线程"+Thread.currentThread().getName()+"的版本号"+stamp);
try {
// 线程t2拿到版本号后,交给线程t1执行ABA操作
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean res = asr.compareAndSet(100, 2019, stamp, stamp+1);
System.out.print(Thread.currentThread().getName()+"\t");
System.out.println("是否修改成功:"+res+"当前最新版本号:"+asr.getStamp()+"当前最新值:"+asr.getReference());
},"t2").start();
打印结果:
当前线程t1的版本号1
当前线程t2的版本号1
t1 当前版本号2
t1 当前版本号3
t2 是否修改成功:false当前最新版本号:3当前最新值
结论:使用使用原子引用+版本号控制可以解决ABA问题,线程t1修改两次后,版本号变化,线程t2在对值修改的时候发现版本号不对,就不会对值进行修改
我的博客:添加链接描述