分享|ABA问题到底如何?
51
8 小时前
8 小时前
发布于 广东

最近在翻看Java中Atomic类源码的时候发现里面居然没有避免ABA问题,问了一下AI,果然是由于Java在早期设计时的并没有考虑到ABA就直接发布了,如果想避免ABA问题可以使用AtomicStamped,或者直接使用自旋锁。当然,我在这里并非想声讨Java,我的本意是研究使用Java的原子类究竟会导致如何程度的ABA问题?然而经过我的不断推演,我得到了一个非常意外的结果,并非仅仅对于Java的包装原子类,我们在通常经典语境下讨论的个ABA问题都不算问题

先让我们回到Java的包装原子类,对于原子类的正常操作,他是有while保证一定会执行的,在这种情况下,发生ABA与否程序都会正常执行,并且执行与否,它都完全不影响结果,因为他并不是加锁,而是直接对数值进行CAS,直接导致每个修改都是非常单纯的修改,顺序与否完全不重要,得到的结果是正确的,如果用我们力扣用户的话来说,就是这段操作的逻辑和正常操作等价,在这种情况下,即使我们引入版本管理的原子类,得到的结果也是完全一样,因此我们可以得出结论,对于Java的原子类的原生相关操作,不存在ABA问题会导致问题,虽然它会发生,但不会导致问题

那么对于经典的aba问题如何?

假设我们存在线程1和2,线程1会判断你的账户余额,然后CAS的扣款100块,线程2会执行两个CAS操作,分别是线程1这样的扣款和存款100块,并且这个操作完全在无锁情况下进行,假设账户中存在100块,那么我们会出现以下情况

线程1准备CAS(存款地址,100,0) >系统调走线程1调出线程2 > 线程2完整执行扣款和存款 > 系统调回线程1,并完成CAS

这种情况下,我们认为出现了ABA问题,从语义上解释线程1他看到的那100块,并非他一开始看到的100块,也就是并非数据没有变动,也就是说他操作的变动的数据,不符合我们对一次CAS操作的预期,当然仅仅是这样,并不足以为ABA定罪,因为他算出的结果依旧正确,于是在此基础上,我们继续认为:

  1. 用户可能存在连续点击行为(卡顿,网络延迟,手滑),而连续点击行为并不意味着我们需要对数据进行连续操作

  2. 整体观察,用户是先取款,然后再取款,然后再存款,在第二次取款时,账户内的金额就已经不够了,不应该能够继续取款,并且还吞掉了后续存款的金额,非常不符合预期

以上就是ABA问题最常见的解释了(反正我了解到这些),但仔细观察上述假设的逻辑,我们发现我们对于问题的描述在ABA问题发生时会发生,在上述程序正常执行中也可能发生,也就是

线程1执行到任意CAS数据准备前(可能是函数调用,可能是前置逻辑代码) >系统调走线程1调出线程2 > 线程2完整执行扣款和存款 > 系统调回线程1,并完成CAS

我们发现在这样的执行逻辑下,和发生ABA问题的情况相比,唯一的区别也只有那个结果正确,但是过程错误的存款了,关键在于我们描述的真正重要的bug,并不是由于ABA导致的,虽然在发生ABA问题的情况下,假如我们能够通过列入版本管理来解决这个问题,但是这个问题却会更高概率的在正常执行逻辑下发生,由此不难发现问题的本质在于多线程安全设计的重大失职而并非ABA问题,而大量的教程博客居然将它简单归结为ABA问题,然后就草草结尾,对于这一点我表示非常的不解和不满,我们为了解释一个不严重的问题,居然设计了一个存在严重问题的环境

于是我们可以得出结论,ABA问题不是问题,吗?

倒也并非如此,事实上如果ABA问题真的不是问题,那么Java也就不会发布AtomicStamped了,仔细观察在上述讨论环境中ABA确实导致的问题,他操作了一个值为预期值,但实际并非为我们所预期值的值,这看起来像是废话,如果是表示计数意义的数字或者字母之类的值,那么我们期望的结果和结果之间并无差距,你的1等于我的1,但如果那个数是(指针/引用)呢,聪明的大家很快就能想到,对于这些超大容器的比较,底层不可能在保持原子性的同时进行这么完整的比较,即使他会深一层的比较,那么深层的深层呢,这个概念非常类似于深拷贝与浅拷贝,所以实际上他只会做简单的地址比较,如果是这种情况,你拿到的值就不仅仅不是你原先的值,甚至无法与它等价,他很有可能被释放了,然后重新利用,再回到你手上,然而你却还认为他并没有被修改过,在这种情况下会确确实实的导致问题发生,对于这种问题也有一个相对经典的描述方式(无锁栈),在此处就不过多提及会导致篇幅过长

所以,我想说面试本质上是公司需求和招聘者努力的双向奔赴,所以背八股没问题,但我希望下次面试官谈及ABA问题的时候,各位可以告诉面试官,ABA问题的本质问题不是值的问题,而是语义的问题

评论 (0)