搞定CAS和ABA问题

1,900 阅读4分钟

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

引言

突发奇想,在写ReentrantLock加锁的过程中,看到compareAndSetState方法调用,就想写一篇文章聊聊CAS操作,以及CAS可能引起的ABA问题。技术大佬可以直接关闭,这个文章确实小儿科,适合基础比较薄弱的同学观看。觉得👌的👨‍🎓,可以在评论区留言,或者点亮❤️支持一下。

CAS

首先,将compareAndSetState方法的源码写在下方。

/**
 * 如果当前状态值等于预期值,则自动将同步状态设置为给定的更新值。
 * 这个操作具有volatile读和写的内存语义。
 * @param expect 期望值
 * @param update 新值
 * @return false返回表示实际值不等于预期值,true表示成功
 */
protected final boolean compareAndSetState(int expect, int update) {
	// See below for intrinsics setup to support this
	return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

什么是CAS?

通过上面的代码可以看到调用了unsafe.compareAndSwapInt,其中compareAndSwap这个就是CAS的全称,比较并交换,这条语句的功能是判断内存中的值,是否与预期值一样,如果一样的话,则更改为新值,这整个过程是原子的,所以是并发安全的操作。

CAS原理

CAS的操作原理是用了乐观锁的概念。每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。这里不断尝试的过程其实就是自旋。
查看unsafe类的源码如下:

/**
 * Atomically update Java variable to <tt>x</tt> if it is currently
 * holding <tt>expected</tt>.
 * @return <tt>true</tt> if successful
 */
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

o就是当前对象,offset是偏移量,unsafe类通过这两个参数值来获取对象在主内存的实际值,与expected值比较,如果不等就♻️下去,不挺比较,直到实际值与expected相等,将期望值x写回主内存。

ABA问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差会导致数据的变化。举个🌰:
比如说一个线程1从内存中取出对象o的值为A,这时候线程2也从内存中取出对象o的值为A,并且线程2进行了一些操作变成了将值变为B,然后线程2又将o的值变成A,这时候线程1进行CAS操作发现内存中仍然是A,然后线程1执行操作将值变成C,操作成功。尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。那么怎样解决这个问题呢?这就需要使用AtomicStampedReference类可以解决ABA问题。

AtomicStampedReference解决ABA问题

这个类维护了一个版本号Stamp,其实有点类似乐观锁的意思。在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);

拿上面的🌰来说,线程1在取出值A时,加上了一个版本号,假设是1,这时线程2取出值A,也加上了版本号,假设也为1,线程2将值变成B时,版本号变成2,再将值变为A时,版本号变成3,此时线程1继续执行,比较主内存中值的版本号为3,与当前线程1拿到的版本号不同,则操作失败,重新从主内存中读取值,取到值的版本号为3,再进行操作,把值变成C。

总结

  • CAS由于使用的是自旋锁,一直循环,开销较大,适用场景在一些并发量不高、线程竞争较少的情况。但是一旦线程冲突严重的情况下,循环时间太长,为给CPU带来很大的开销。
  • 只能保证一个变量的原子操作,多个变量依然要加锁。引出了ABA问题(使用AtomicStampedReference可解决)。