题目:什么是CAS?存在什么问题?
难度:low
标签:java/cas, general/knowledge
评分:⭐⭐⭐☆☆
CAS 其实是对线程安全问题的一种处理方式,它本身的处理方式指的是比较,然后交换。如果比较的过程中发现比较的对象发生了变化,就先不做处理,进行再一次比较,然后再去进行处理,然后确保修改修改的时候是符合自身预期的,然后来实现线程安全的保障。但是它本身存在一个所谓 ABA 的问题。我修改前这个目标是 A,但是中间的过程中有另一个线程把目标修改为了 B,而我修改的时候预期是把 A 修改成其他的,但是它已经是 B 了。而这中间又出现了一个线程,把这个 B 改成了 A,但是我修改的时候看到的还是 A。实际上本身预期是我从 A 改到另一个值,但实际上我改的是有一个人把 A 改成了 B,另一个人把 B 又改成了 A。我修改的实际内容并不是我原本预期的,这就是 CAS 中最常见的,也是最核心的问题,ABA 问题。它处理的时候可以使用版本号,就不存在 ABA就是你每次只能往下更改,不会说 A 又能改回去的情况。
优点:准确抓住 CAS = Compare And Swap 的本质;ABA 问题描述清晰(A→B→A 全过程);正确给出版本号解决方案
不足:只覆盖 ABA 一个痛点,遗漏自旋开销、单变量原子性;未说明 CAS 在 OS 层的实现(cmpxchg 硬件指令);未提 AtomicStampedReference 官方实现
补充:三要素 expected/actual/new;三大痛点 ABA、自旋开销、单变量原子性;AtomicStampedReference 是版本号的官方实现
核心考点定位:能否说清 CAS 的三要素、OS 层面实现,以及三个常见痛点。
知识点拆解:
expected(期望值):线程认为内存中的当前值actual(实际值):内存中真实的值new(新值):期望交换成的值只有当 actual == expected 时才把内存改为 new,否则什么都不做(CAS 失败,通常自旋重试)
cmpxchg,单条指令完成比较+交换,不会被中断打断;Java 里通过 Unsafe 或 VarHandle 调用,是用户态的"无锁"原语按「底层原理 → 设计权衡 → 工程实践」递进排列。
考察方向:
延伸递进面试题:
面试官您好,CAS 全称 Compare And Swap,是并发编程里最基础的无锁原子操作。
它的三个要素是 expected、actual 和 new:线程先读一下内存值,作为 expected;如果内存中的实际值 actual 等于 expected,就把内存改成 new;否则什么都不做,通常自旋重试。CAS 之所以能"无锁",是因为它依赖 CPU 的原子指令,x86 上是 cmpxchg,一条指令完成比较+交换,不会被中断打断,也不用进内核态拿锁。
CAS 有三个常见的问题:
第一是 ABA 问题。我先看到值是 A,想把它改成 C;但我准备执行 CAS 的这段时间里,另一个线程把 A 改成了 B,紧接着又把它改回了 A;这时候我的 CAS 看到还是 A,就成功了,但实际上这个"A"已经和当初的"A"不是同一个东西了,中间的业务语义全错了。解决方案是引入版本号,比如 Java 的 AtomicStampedReference 就是"值 + 版本号"配对,每次修改都让版本号递增,ABA 自然被识别出来。
第二是自旋开销。CAS 失败要重试,如果竞争激烈,线程就一直自旋占着 CPU,不像锁那样会阻塞让出。极端情况下 CPU 会被打满。工程上遇到这种场景常用 LongAdder,它把累加器拆成 Cell 数组,多线程各写各的分段,读的时候再汇总。
第三是单变量原子性。CAS 只能保证一个变量的原子,如果你要做 a++ 和 b-- 这种复合操作,CAS 管不住,还是得回到 synchronized 或者用复合类型的原子方法。
典型应用上,AtomicInteger、AtomicLong、AtomicReference 都是基于 CAS 的,ConcurrentHashMap 的 put 也是靠 cell 级别的 CAS 来保证并发安全。
这就是我对 CAS 的理解。