226、什么是ABA问题?

上篇博客最后讲到了CAS会导致“ABA问题”,那到底什么是ABA问题呢?下面介绍一下。

CAS算法实现一个重要前提需要取出内存中某一个时刻的数据并在当下时刻做比较和替换。从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

我们先再引入一个概念——原子引用。java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBooleanAtomicIntegerAtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。但是对于用户自定义的类该怎么办呢?这时候就用到了java.util.concurrent.atomic这个包下面有一个类:AtomicReference<V>,如果想对某一个自定义的类进行原子包装,就需要使用这个类。

思考下面一个问题:比如说一个线程one某一时刻从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作,首先将值变为了B,然后线程two又将V位置的数据从B变为了A,这时候线程one进行CAS操作发现内存中V位置的值依旧是A,然后线程one操作成功。尽管线程one的CAS操作成功了,但是并不代表这个过程中就不会产生问题。这就是CAS的ABA问题。

表面看这个问题不会带来什么影响,但是如何和实际应用场景结合起来,就可以看出问题所在了。我们举一个提款机的例子:

假设有一个遵循CAS原理的提款机,小慧有100元存款,要使用这个提款机来提款50,由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50.....流氓,还我钱。。

如何解决ABA问题呢?JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。如果当前引用 == 预期引用,并且当前标志等于预期标志,则以原子方式将该引用和该标志的值设置为给定的更新值。

其实解决的办法很简单,加个版本号就可以了。什么意思呢?真正要做到严谨的CAS机制,我们在Compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。用例子来说明一下,假设地址V中存储着变量值A,当前版本号是01。线程1获得了当前值A和版本号01,想要更新为B,但是被阻塞了。这时候,内存地址V中的变量发生了多次改变,版本号提升为03,但是变量值仍然是A。随后线程1恢复运行,进行Compare操作。经过比较,线程1所获得的值和地址V的实际值都是A,但是版本号不相等,所以这一次更新失败。在Java当中,AtomicStampedReference类就实现了用版本号做比较的CAS机制。