当前位置: 首页 > 科技观察

CAS和ABA问题及解决方法

时间:2023-03-13 14:06:33 科技观察

要理解ABA问题,首先要知道什么是CAS。CAS的全称是compareandswap,是一种在多线程环境下实现同步功能的机制。CAS的出现主要是为了解决多线程并发情况下数据不一致的问题。CAS的底层原理CAS的思想很简单:三个参数,一个当前内存值V,旧的期望值A,和要更新的值B,当且仅当期望值A和内存值V同理,修改内存值为B,返回true,否则什么都不做,返回false(native)方法,基于此类可以直接操作特定内存的数据。Unsafe类存在于sum.misc包中,其内部实现是用C++编写的。我从JDK1.8源码UnsafeWrapper("Unsafe_CompareAndSwapInt");oopp=JNIHandles::resolve(obj);截取关键代码UNSAFE_ENTRY(jboolean,Unsafe_CompareAndSwapInt(JNIEnv*env,jobjectunsafe,jobjectobj,jlong??offset,jinte,jintx));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是否为多核。如果是多核,加锁内存屏障,防止多线程并发竞争比较交换,调用汇编命令cmpxchg获取新值并设置。CAS问题cas实现从JDK1.5开始,java.util.concurrent包为我们提供了很多cas操作类如:AtomicInteger、AtomicLong、AtomicReference,提供了轻量级的锁机制,性能更好,但同时会有一些问题,我们用一张图来说明:上图运行过程中可能会出现两个问题:线程3可能一直获取不到最新的值,导致线程自旋,有一个数据值在主存:A,两个线程A和B分别将主存数据拷贝到各自的工作空间中。A执行缓慢,需要10秒。B执行得更快,需要2秒。这时线程B把主存中的数据改成了B,过了一会又改成了A,然后A线程进行了比较,发现结果是A,以为别人没有触摸它,然后执行更改操作。其实中间已经变了,这就是ABA的问题。ABA问题的优化ABA问题的原因是在CAS过程中简单地检查了“值”。在某些情况下,相同的“值”不会引入错误的业务逻辑(比如库存)。在某些情况下,“值”虽然相同,但不再是原来的数据。那么如何避免ABA问题呢?优化的方法也很简单,就是我们可以通过对值进行标注而不是仅仅比较值来很好的避免ABA问题。JAVA也为我们提供了相应的处理类AtomicStampReferenceAtomicStampReference在cas的基础上增加了标记戳记。使用这个标记可以用来检测数据是否发生了变化,给数据带来一种有效性检验。它有如下几个参数://参数的含义分别是期望值、写入的新值、期望标记、新标记值publicbooleancompareAndSet(Vexpected,VnewReference,intexpectedStamp,intnewStamp);publicVgetRerference();publicintgetStamp();publicvoidset(VnewReference,intnewStamp);下面用一个例子来说明:publicclassTest{privatestaticAtomicReferenceatomicReference=newAtomicReference(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的值时,版本号都会加1。AtomicStampedReference内部维护对象值和版本号。在创建AtomicStampedReference对象时,需要传入初始值和初始版本号。当AtomicStampedReference设置对象值时,对象值和状态戳必须满足预期值,写入才会成功。publicclassTest{privatestaticAtomicStampedReferenceatomicStampedReference=newAtomicStampedReference(100,1);publicstaticvoidmain(String[]args){newThread(()->{System.out.println("t1获取的初始版本号:"+atomicStampedReference.getStamp());//休眠1秒,让t2线程也得到相同的初始版本号+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)+"\tcurrentvalue:"+atomicStampedReference.getReference());},"t2").start();}}初始值100,初始版本号为1。线程t1和t2获得相同的初始版本号。线程t1完成ABA操作,版本号增加到3。线程t2完成CAS操作。最新的版本号变成了3,和之前线程t2获取到的版本号一样。1不等于,操作失败输出结果:t1得到的初始版本号:1t2得到的初始版本号:1最新版本号:3false当前值:100【编者推荐】Windows10的这个功能已经被禁用了!教你如何彻底关闭C++,C++程序员谁先完蛋?2021年值得关注的人工智能趋势RAID磁盘阵列适合你吗?在一篇文章中了解Windows10是绝唱!微软新系统开始更改版本号