CAS操作
1.说一下CAS及底层硬件实现
CAS是一种乐观锁机制、通过比较内存中的值与预期值是否相等来决定是否更新、是实现原子操作的基础。
CAS 操作涉及三个操作数:
-
内存位置(V):需要操作的变量的内存地址。
-
预期值(A):期望变量当前的值。
-
新值(B):希望将变量更新为的新值。
CAS 操作的步骤如下:
-
比较内存位置 V 的值是否等于预期值 A。
-
如果相等、则将内存位置 V 的值更新为新值 B。
-
如果不同、就重试也就是自旋、直到成功为止。
CAS 操作是原子的、即在执行过程中不会被中断、确保了线程安全
底层实现
在 Java 中,CAS 操作的调用链如下:
Java 代码 → Unsafe 类(native 方法)→ JVM 内部 C++ 实现 → CPU 原子指令
Java 使用 sun.misc.Unsafe
类调用本地方法、底层通过 JVM 实现、再映射到操作系统所支持的 CPU 原子指令。
Java 能在不同架构上实现 CAS、是因为各主流 CPU 都提供了支持原子操作的指令集。
在x86架构中:
-
使用的是LOCK CMPXCHG指令实现CAS。cmpxchg 指令会比较寄存器中的值和内存中的值、如果相等、则将另一个寄存器中的值写入内存。否则不做任何操作。LOCK 前缀 保证操作期间的总线锁定、从而实现原子性。
在ARM架构中则是通过LDREX和STREX指令对实现的
-
LDREX指令:加载用于加载内存中的值并标记目标内存为独占访问
-
STREX:用于将值尝试写入该内存、但只有在该内存位置仍然被标记为独占访问时才会成功
CAS 如何保证原子性?
-
不同 CPU 提供特定的原子指令(如 x86 的 LOCK CMPXCHG、ARM 的 LDREX/STREX);
-
这些指令在执行过程中是不可中断的、CPU 从指令层面保障了操作的原子性;
-
因此CAS 在硬件级别上就是线程安全的
2.CAS的缺点ABA问题
-
ABA问题:ABA的问题指的是在CAS更新的过程中、当读取到的值是A、然后准备赋值的时候仍然是A、但是实际上有可能的值在中途的时候被改成了B、然后又被改回了A、导致线程误以为变量未被修改、可能引发线程安全问题。
举个例子:
什么是 ABA 问题
假设线程 T1 执行了如下 CAS 操作:
// T1 希望把变量从 A 改成 C if (value == A) { value = C; }
✅ 但是在 T1 执行 CAS 期间、发生了这样的事:
-
T1 读取到变量是 A。
-
T1 暂停/卡顿了一下(还没来得及执行 CAS)。
-
线程 T2 把变量从 A 改成了 B,然后又改回了 A。
-
T1 恢复执行,看变量还是 A,于是以为没变过,就把它改成了 C。
❗ 问题来了:
从 T1 的角度看、变量值从头到尾都是 A、CAS 好像是安全的。
但其实:
-
变量经历了 A → B → A 的变化,
-
状态被别人改动过,
-
这中间的变化 T1 完全不知道!
✅ 举个现实生活的例子:
你把手机放在桌子上(值是 A)、然后你去洗手。
洗手回来后、看到手机还在桌子上(值看起来没变,还是 A)、你以为没人动过。
但其实你室友来过:
-
拿起手机(变成 B),
-
看了几秒钟,
-
又放回原处(变成 A)。
你以为没人动、其实被动过了!
这就是典型的 ABA 问题。
这个CAS更新的漏洞就叫做ABA。ABA 问题可能会让你错判数据状态、导致数据错误或线程安全问题
ABA问题的解决思路:使用版本号。在变量前面追加上版本号、每次变量更新的时候把版本号加1、那么A->B->A就会变成1A->2B->3A。
-
Java中有
AtomicStampedReference
来解决这个问题、这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引、,并且检查当前标志是否等于预期标志、如果全部相等、则以原子方式将该引用和该标志的值设置为给定的更新值。 -
CAS是无锁的、线程之间存在竞争的时候只能通过自旋来等待、自旋CAS的方式如果长时间不成功、会导致 CPU 占用高、性能下降。
❗ 自旋 CAS 的缺点:
高并发冲突下、CPU 消耗很大
-
在线程冲突非常激烈的情况下(比如 10 个线程同时 CAS 一个变量),
-
只有一个线程能成功、其他线程会不断地失败 → 重试 → 再失败 → 再重试…
-
这些线程虽然没阻塞、但仍然在疯狂占用 CPU 做无效操作。
所以说:
❗ 自旋 CAS 在高并发+冲突严重场景下、会导致 CPU 占用高、性能下降。
✅ 怎么优化这个问题?
-
加退避机制→ 每次 CAS 失败后、不立即重试、而是稍微 sleep 一段随机时间。
-
限制自旋次数、失败就挂起→ 如果 CAS 尝试超过 N 次失败、就放弃自旋、转为挂起等待。
-
JDK 优化:Unsafe、VarHandle、LongAdder 等底层类都有更好的冲突控制策略
-
只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性、但是对多个共享变量操作时则不不能保证原子性、多个可以通过
AtomicReference
来处理或者使用锁synchronized
实现
3.说一下自旋锁
首先来说一下阻塞是线程是竞争锁失败后被挂起进入一种等待锁释放的状态
自旋锁是一种非阻塞式的锁实现方式、线程在尝试获取锁失败时不会进入阻塞或者挂起状态、而是通过循环自旋的方式等待锁的释放
工作原理是:
-
当一个线程尝试获取锁、如果锁被占用、线程获取锁失败、但它不会立即进入阻塞或者挂起状态、而是继续尝试获取锁。
-
这个的自旋的意思就是它会在循环中使用类似 CAS 的方式不断尝试获取锁
-
在自旋期间、线程保持活跃状态、占用 CPU、不会主动让出。
但是
-
若锁竞争激烈或临界区耗时长、自旋会导致 CPU 空转、系统吞吐降低
-
因此在 JDK 或操作系统中、自旋锁常结合自旋一定次数后挂起线程策略使用
参考面试回答
面试官:说一下CAS及底层硬件实现
CAS是一种乐观锁机制、通过比较内存中的值与预期值是否相等来决定是否更新、是实现原子操作的基础。
CAS 操作涉及三个操作数:
-
内存位置(V):需要操作的变量的内存地址。
-
预期值(A):期望变量当前的值。
-
新值(B):希望将变量更新为的新值。
CAS 操作的步骤如下:
-
比较内存位置 V 的值是否等于预期值 A。
-
如果相等、则将内存位置 V 的值更新为新值 B。
-
如果不同、就重试也就是不断的自旋、不断的检查某个变量是否等于期望值直到成功为止。
CAS 操作是原子的、即在执行过程中不会被中断、确保了线程安全
底层实现
在 Java 中,CAS 操作的调用链如下:
Java 代码 → Unsafe 类(native 方法)→ JVM 内部 C++ 实现 → CPU 原子指令
Java 使用 sun.misc.Unsafe
类调用本地方法、底层通过 JVM 实现、再映射到操作系统所支持的 CPU 原子指令。
Java 能在不同架构上实现 CAS、是因为各主流 CPU 都提供了支持原子操作的指令集。
在x86架构中:
-
使用的是LOCK CMPXCHG指令实现CAS。cmpxchg 指令会比较寄存器中的值和内存中的值、如果相等、则将另一个寄存器中的值写入内存。否则不做任何操作。LOCK 前缀 保证操作期间的总线锁定、从而实现原子性。
在ARM架构中则是通过LDREX和STREX指令对实现的
-
LDREX指令:加载用于加载内存中的值并标记目标内存为独占访问
-
STREX:用于将值尝试写入该内存、但只有在该内存位置仍然被标记为独占访问时才会成功
CAS 如何保证原子性?
-
不同 CPU 提供特定的原子指令(如 x86 的 LOCK CMPXCHG、ARM 的 LDREX/STREX);
-
这些指令在执行过程中是不可中断的、CPU 从指令层面保障了操作的原子性;
-
因此CAS 在硬件级别上就是线程安全的
CAS的缺点
-
首先是有ABA问题:ABA的问题指的是在CAS更新的过程中、当读取到的值是A、然后准备赋值的时候仍然是A、但是实际上有可能的值被改成了B、然后又被改回了A。这个CAS更新的漏洞就叫做ABA。
-
Java中有
AtomicStampedReference
来解决这个问题、这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引、,并且检查当前标志是否等于预期标志、如果全部相等、则以原子方式将该引用和该标志的值设置为给定的更新值。
-
-
CAS是无锁的、线程之间存在竞争的时候只能通过自旋来等待、自旋CAS的方式如果长时间不成功、会导致 CPU 占用高、性能下降。
-
只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性、但是对多个共享变量操作时则不不能保证原子性、多个可以通过
AtomicReference
来处理或者使用锁synchronized
实现