要了解ABA问题,我们得先知道什么是CAS,CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS的出现主要是为了解决多线程并发情况下,数据的不一致问题。
CAS底层原理
CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false
Unsafe类
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,基于该类可以直接操作特定内存的数据。Unsafe类存在与sum.misc包中,其内部实现是C++写的,我从JDK1.8源码中截取了关键代码
UNSAFE_ENTRY(jboolean,Unsafe_CompareAndSwapInt(JNIEnv*env,jobjectunsafe,jobjectobj,jlongoffset,jinte,jintx))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oopp=JNIHandles::resolve(obj);
jint*addr=(jint*)index_oop_from_field_offset_long(p,offset);
return(jint)(Atomic::cmpxchg(x,addr,e))==e;
UNSAFE_END
从上面代码可以看出最后调用的是Atomic:comxchg这个方法,这个方法的实现放在hotspot下的os_cpu包中,说明这个方法的实现和操作系统、CPU都有关系,以多核CPU为例:
- 首先会判断CPU是否为多核,如果是多核加一个lock内存屏障,这样就可以防止多线程并发情况竞争发生
- 进行对比交换,调用汇编指令cmpxchg获取新值并设值。
CAS问题
cas实现
从JDK1.5开始,java.util.concurrent包为我们提供了许多cas操作类诸如:AtomicInteger,
AtomicLong,AtomicReference,它提供了轻量级的锁机制有着更好的性能,但同时也会出现一些问题,我们通过一张图来说明:
上图运行过程中可能会出现两个问题:
- 线程3可能一直拿不到最新的值,导致线程自旋
- 主内存有个数据值:A,两个线程A和B分别copy主内存数据到自己的工作区,A执行比较慢,需要10秒, B执行比较快,需要2秒, 此时B线程将主内存中的数据更改为B,过了一会又更改为A,然后A线程执行比较,发现结果是A,以为别人没有动过,然后执行更改操作。其实中间已经被更改过了,这就是ABA问题。
ABA问题的优化
ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。那如何能避免ABA问题呢?优化的方式也很简单,就是不能只对值进行比较,通过对值打标签的方式就能很好的避免ABA问题。JAVA中也为我们提供了相应的处理类AtomicStampReferenceAtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:
//参数代表的含义分别是期望值,写入的新值,期望标记,新标记值
publicbooleancompareAndSet(Vexpected,VnewReference,intexpectedStamp,intnewStamp);
publicVgetRerference();
publicintgetStamp();
publicvoidset(VnewReference,intnewStamp);
我们通过一个示例来说明:
publicclassTest{
privatestaticAtomicReference<Integer>atomicReference=newAtomicReference<Integer>(100);
publicstaticvoidmain(String[]args){
newThread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
newThread(()->{
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100,2021)+"\\t修改后的值:"+atomicReference.get());
},"t2").start();
}
}
- 初始值为100,线程t1将100改成101,然后又将101改回100
- 线程t2先睡眠1秒,等待t1操作完成,然后t2线程将值改成2019
可以看到,线程2修改成功。输出结果:
true修改后的值:2021
要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号, 当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功
publicclassTest{
privatestaticAtomicStampedReference<Integer>atomicStampedReference=newAtomicStampedReference<Integer>(100,1);
publicstaticvoidmain(String[]args){
newThread(()->{
System.out.println("t1拿到的初始版本号:"+atomicStampedReference.getStamp());
//睡眠1秒,是为了让t2线程也拿到同样的初始版本号
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
},"t1").start();
newThread(()->{
intstamp=atomicStampedReference.getStamp();
System.out.println("t2拿到的初始版本号:"+stamp);
//睡眠3秒,是为了让t1线程完成ABA操作
try{
TimeUnit.SECONDS.sleep(3);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println("最新版本号:"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(100,2021,stamp,atomicStampedReference.getStamp()+1)+"\\t当前值:"+atomicStampedReference.getReference());
},"t2").start();
}
}
输出结果:
t1拿到的初始版本号:1
t2拿到的初始版本号:1
最新版本号:3
false当前值:100
原文地址:https://mp.weixin.qq.com/s/jFnefSRTybY8PeogRs8rRQ



