Java原子类中可以靠版本号比较为什么还需要CAS操作?以及ABA问题到底是什么?

2022/3/21 22:58:57

本文主要是介绍Java原子类中可以靠版本号比较为什么还需要CAS操作?以及ABA问题到底是什么?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

https://www.zhihu.com/question/269109328

问题

最近面试面试官提到java原子类可以通过CAS保证操作的原子性,但缺点是产生了ABA问题,所以可通过版本号比较。那为什么不直接通过版本号比较呢? 这是一个问题,我还有一个疑惑就是ABA问题在什么情况下会造成影响,值一样不就行了吗。

回答

1

的确可以只比较版本号,实际上一些外部KV存储的CAS经常就是只比较版本号的,但是你的目标并不是原子性地递增一个版本,而是原子性地在递增版本的同时设置值,保证这两个操作一起形成一个原子操作。所以需要拼一下。如果只想看版本是否更新,就不需要。

CAS的价值并不是说最后更新成正确的值就好,如果这样那只要每次都直接set就好了。它的价值是形成乐观锁,保证在两次commit之间没有其他commit,很多情况下从读取前一个值到CAS之间的过程中,并不仅仅用到了这个数值。

最典型的情况下,这个值可以是个引用(指针),操作者读取了引用指向的对象内部的内容,创建新对象,然后将新对象通过CAS设置上去,完成一次事务。如果这个过程中发生了ABA,回到A的时候,虽然指向的地址没有变化,但对象内部的内容可能发生了变化,这就会导致错误

2

我觉得java的AtomicStampedReference确实蛮坑爹的,每次cas不管成功失败都会new出一个对象,性能有一定影响。最好的解法是像c++的folly库一样能偷64位指针的高16位做stamp,但是java语言层面规定的太死,玩不了这种hack。

不要用Reference,而用数组下标做指针,用Atomic记录下标,偷高位的bit做stamp,可以绕开AtomicStampedReference,但这样GC又成了问题…反正挺麻烦的。

ABA给你举个例子你就懂了,假如我有个链表W->X->Y->Z,我要删除X,如果我只做W.next.CompareAndSet(X, X.next)的话,如果这个线程CAS前被挂起,另一个线程删掉了X,X被GC了,又另一个线程new了个node,叫nX好了,插入到W和Y中间,而刚好分配给他X原来的地址,链表就变成了 W->nX->Y->Z,这时候第一个线程醒了,CompareAndSet仍然会成功,因为X和nX地址一样,就把nX删掉了,而逻辑上X和nX是不同的node,第一个线程逻辑正确的操作是不去动链表,返回false(没找到X)。如果用了StampedReference每次修改以后stamp+1,就可以避免这种情况。

3

不熟悉什么java原子类怎么还有版本号一说,CAS是动作,版本号是为了确认引用的对象还之前读到的那个。

ABA是指引用(指针)值没变,但实际对象变化了(新生成的),之前那个已经被回收了,所以版本号严格递增,就知道这个改变是否发生过了。

理论上版本号也不是一个合法的方案,因为版本号也会溢出循环。实际应用中考虑这个时间周期一般很长,就认为几乎不可能遇到。如果是为了合成为一个原子操作,那么就肯定都要比较了,因为CAS不是为检查版本号而设计的,而且它被发明的时候没有理论的指导,就是觉得简单又强大。

后来理论上说的是只要CAS就能干一大票事了,但实践中想要达到理论的效果,似乎又总有点力不从心的地方,所以变着花样从各个角度去增强它。



这篇关于Java原子类中可以靠版本号比较为什么还需要CAS操作?以及ABA问题到底是什么?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程