更多数据结构,算法,设计模式,源码分析等请关注我的微信公众号[技术寨],每周至少两篇优质文章
目录
1.引入
为了说明CAS机制,需要先介绍一下悲观锁和乐观锁。
1.1 悲观锁
总是假设最坏的情况,每次操作数据之前都会先上锁,后面想要取数据就会被阻塞。在java中通过synchronized同步原语来实现悲观锁。
悲观锁存在的问题:在多线程环境下,在等待锁,释放锁时会存在线程的上下文切换导致性能下降。
1.2 乐观锁
总是假设最好的情况,每次操作数据之前都不会上锁,在更新时判断数据是否已经是否已经被更新。其常见的一种实现方式就是今天的主角:CAS。
2.CAS
2.1 三个操作数
- 内存地址 V
- 旧的预期值 A
- 新值 B
如果内存地址V的值是A,就会更新成B,否则将不断自旋。
用java语言表示CAS,其形式一般是:
/**
* @param address 内存地址
* @param except 旧的预期值
* @param update 新值
* @return true表示操作成功,否则失败
**/
<T> boolean cas(long address,T except,T update)
2.2 CAS存在的问题
2.2.1 ABA问题
描述:线程1将数据从A->B,线程2又将数据从B->A,此时如果线程3的希望值和线程1当时一样都是A就会出现问题,因为此时A非彼时的’A’。
解决方式:由于我们已经分析出问题的原因是我们只是单纯通过期望的内存值无法判断是否中间存在更新的情况,所以我们需要再加一个标识用于表示内存值更新状态,一般我们可以使用一个递增的版本号实现。具体可以参考atomic类的AtomicStampedReference
和AtomicMarkableReference
。
2.2.2 自旋时间长
在高并发的情况下,有N个线程同时在执行CAS操作,只有一个线程能成功,其他都需要重新自旋,会消耗大量的时间。
2.2.3 只能保证一个变量的原子性操作
CAS只能操作一个内存地址,即一个变量。如果需要同时控制多个变量怎么呢?我们可以将多个变量封装在一起,这样不就又变成一个变量了。
3. CAS例子与说明
由于java的原子类主要就是通过CAS机制实现的,所以我们看一下AtomicInteger
这个原子类的updateAndGet
方法,它比较清晰地展示了CAS的处理方式。
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
// 获取内存地址V的旧址A
prev = get();
// 计算新值B
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
// CAS操作
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
说明一下:
- 这里的compareAndSet就是我们介绍的CAS,这里的unsafe.compareAndSwapInt为什么会存在4个参数,不是只需要address,except和update这三个参数吗?因为我前面已经说了,一般式确实只需要这三个参数,它也是最核心的参数。不过 unsafe.compareAndSwapInt前两个参数的含义是当前对象实例和操作数的相对该对象的偏移地址,所以它们就可以计算出address。
- 这里可以看到CAS的一种常见的使用结构,如果需要原子性更新一个变量时可以参考一下。(不过原子类已经帮我们封装好了,几乎不会这种底层结构)
address = xxx;
do {
// 获取内存地址V的旧址A
prev = xxx;
// 计算新值B
next = xxx;
} while (!compareAndSet(address, prev, next));