Java并发编程:CAS操作

发布于:2025-05-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

1. 简介

  CAS 全称是 Compare-and-Swap,即比较加交换,假设我们要对内存中的某个地址进行 CAS 操作,该地址当前值是 V,我们给定预期值 A 和新值 B,如果 V == A,那么就将该地址的值更新为 B 并返回 true,否则什么也不做只返回 false,不过这些步骤必须要原子地进行;从 CAS 的执行过程我们可以看到,CAS 的本质就是原子地对某一个内存地址先进行一致性检查,如果一致性检查通过,则执行写入操作,返回成功,如果一致性检查不通过,则返回失败,通知调用方,该内存地址发生了冲突。CAS 是如何保证操作时原子的呢,这是由底层处理器的原子原语来保证的,因此能否执行 CAS 操作,需要 CPU 的支持,当然现代的处理器都时支持 CAS 操作的,处理器提供的 CMPXCHG 指令就是执行 CAS 操作的。Java 对 CAS 的支持是通过 sun.misc.Unsafe 类来支持的,Unsafe 内部提供了本地方法用于对内存中的某一个地址执行 CAS 操作。

2. CAS 的优势和局限性

CAS的优势

CAS 支持对某一个内存地址进行原子的检查 + 更新的复合操作,在很多场合下,可以代替系统互斥量和信号量,减少应用程序的系统调用,并且可以大大减少锁的使用,无锁的非阻塞同步算法就是基于 CAS 进行设计和实现的,这些非阻塞同步算法能够大大提升系统的并发性能和可伸缩性,并且能够很好地防止活跃性故障的发生。

CAS的局限性
  1. CAS 只能对一个内存地址的值进行单一的检查 + 改写的原子操作,因此使用 CAS 进行同步算法设计非常复杂,需要引入复杂的数据结构,这些复杂性会带来很多额外的开销,并且极易出错;
  2. CAS 操作很难将多个对像的多个调用组合成一次原子调用,随着需要同步的变量数增加,使用 CAS 来设计无锁同步算法的复杂度就会指数级上升,最终变得不可能。因此,CAS 适合应用于共享变量很少的场合。
  3. 使用 CAS 设计的同步算法可能带来 ABA 问题,为了解决 ABA 问题会引入额外的性能开销。注意并非 CAS 本身具有ABA问题,而是基于 CAS 设计的同步算法。
ABA问题

下面的代码是 CAS 的典型应用模式,在第 7 行,将原子变量 v 的值保存在本地的副本 oldVal 上面,用于后面做一致性检查,第 8 行计算新值,第 9 行执行 CAS 操作,如果在执行 CAS 操作的时候发现 v 的值与本地的副本 oldVal 不相等,那么说明检测到了冲突,有其他线程正在并发调用 calac 方法,那么就要进行失败重试,但是有一种情况,也发生了冲突,但是却能够通过一致性检查,比如当前线程执行完第7行(此时 v 的值是 A,oldVal 的值也是 A)但还未执行第9行时,有1个线程成功将 v 值从 A 改成了 B,而另外一个线程又成功将 v 的值从 B 改回 A,v 的值发生 A -> B -> A 的变化,这时当前线程执行到第9行,因为 v 的当前值等于其副本 oldVal,所以成功通过了一致性检查,成功更新了 v 的值,这时候就发生了 ABA 错误,ABA 错误导致冲突检查机制失效,当前线程并未检测到实际已经发生了的冲突。

 1 -> 	public class Demo {
 2 -> 	
 3 -> 		private final AtomicInteger v = new AtomicInteger();
 4 -> 	
 5 -> 		public void calac() {
 6 -> 			while (true) {
 7 -> 				int oldVal = v.get();
 8 -> 				int newVal = calacNewVal(oldVal);
 9 -> 				if(v.compareAndSet(oldVal, newVal)) {
10 -> 					break;
11 -> 				}
12 -> 			}
13 -> 		}
14 -> 	
15 -> 		int calacNewVal(int oldVal) {
16 -> 	    	...
17 -> 		}
18 -> 	}

ABA 问题并不是一定会发生,当原子变量的值不会重复时,比如变量的值是单调增加或单调递减的,这种情况下就不会出现 ABA 问题。一般我们使用原子变量是用来做同步计数器,因此它的值是递增的,所以当使用原子变量做计数器时无需担心 ABA 问题,但是对于原子引用类 AtomicReference,因为它的值(引用地址)是完全随机的,而且 AtomicReference 一般应用于各种无锁的链式数据结构中,因此需要考虑解决 ABA 问题,JDK 也提供了带有版本戳的原子引用类 AtomicStampedReference,能够避免 AtomicReference 的 ABA 问题,但同时也带来了额外的存储空间消耗和性能开销。


网站公告

今日签到

点亮在社区的每一天
去签到