https://www.zhihu.com/question/269109328
最近面试面试官提到java原子类可以通过CAS保证操作的原子性,但缺点是产生了ABA问题,所以可通过版本号比较。那为什么不直接通过版本号比较呢? 这是一个问题,我还有一个疑惑就是ABA问题在什么情况下会造成影响,值一样不就行了吗。
的确可以只比较版本号,实际上一些外部KV存储的CAS经常就是只比较版本号的,但是你的目标并不是原子性地递增一个版本,而是原子性地在递增版本的同时设置值,保证这两个操作一起形成一个原子操作。所以需要拼一下。如果只想看版本是否更新,就不需要。
CAS的价值并不是说最后更新成正确的值就好,如果这样那只要每次都直接set就好了。它的价值是形成乐观锁,保证在两次commit之间没有其他commit,很多情况下从读取前一个值到CAS之间的过程中,并不仅仅用到了这个数值。
最典型的情况下,这个值可以是个引用(指针),操作者读取了引用指向的对象内部的内容,创建新对象,然后将新对象通过CAS设置上去,完成一次事务。如果这个过程中发生了ABA,回到A的时候,虽然指向的地址没有变化,但对象内部的内容可能发生了变化,这就会导致错误
。
我觉得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,就可以避免这种情况。
不熟悉什么java原子类怎么还有版本号一说,CAS是动作,版本号是为了确认引用的对象还之前读到的那个。
ABA是指引用(指针)值没变,但实际对象变化了(新生成的),之前那个已经被回收了
,所以版本号严格递增,就知道这个改变是否发生过了。
理论上版本号也不是一个合法的方案,因为版本号也会溢出循环。实际应用中考虑这个时间周期一般很长,就认为几乎不可能遇到。如果是为了合成为一个原子操作,那么就肯定都要比较了,因为CAS不是为检查版本号而设计的,而且它被发明的时候没有理论的指导,就是觉得简单又强大。
后来理论上说的是只要CAS就能干一大票事了,但实践中想要达到理论的效果,似乎又总有点力不从心的地方,所以变着花样从各个角度去增强它。