当前位置: 首页 > news >正文

CAS操作

1.说一下CAS及底层硬件实现

CAS是一种乐观锁机制、通过比较内存中的值与预期值是否相等来决定是否更新、是实现原子操作的基础。

CAS 操作涉及三个操作数:

  1. 内存位置(V):需要操作的变量的内存地址。

  2. 预期值(A):期望变量当前的值。

  3. 新值(B):希望将变量更新为的新值。

CAS 操作的步骤如下:

  1. 比较内存位置 V 的值是否等于预期值 A。

  2. 如果相等、则将内存位置 V 的值更新为新值 B。

  3. 如果不同、就重试也就是自旋直到成功为止。

CAS 操作是原子的、即在执行过程中不会被中断、确保了线程安全

底层实现

在 Java 中,CAS 操作的调用链如下:

Java 代码 → Unsafe 类(native 方法)→ JVM 内部 C++ 实现CPU 原子指令

Java 使用 sun.misc.Unsafe 类调用本地方法、底层通过 JVM 实现、再映射到操作系统所支持的 CPU 原子指令。

Java 能在不同架构上实现 CAS、是因为各主流 CPU 都提供了支持原子操作的指令集。

x86架构中:

  • 使用的是LOCK CMPXCHG指令实现CAScmpxchg 指令会比较寄存器中的值和内存中的值、如果相等、则将另一个寄存器中的值写入内存。否则不做任何操作。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 期间、发生了这样的事:

  1. T1 读取到变量是 A。

  2. T1 暂停/卡顿了一下(还没来得及执行 CAS)。

  3. 线程 T2 把变量从 A 改成了 B,然后又改回了 A

  4. 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 占用高、性能下降。

✅ 怎么优化这个问题?

  1. 加退避机制→ 每次 CAS 失败后、不立即重试、而是稍微 sleep 一段随机时间。

  2. 限制自旋次数、失败就挂起→ 如果 CAS 尝试超过 N 次失败、就放弃自旋、转为挂起等待。

  3. JDK 优化:Unsafe、VarHandle、LongAdder 等底层类都有更好的冲突控制策略

  • 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性、但是对多个共享变量操作时则不不能保证原子性、多个可以通过AtomicReference来处理或者使用锁synchronized实现

3.说一下自旋锁

首先来说一下阻塞是线程是竞争锁失败后被挂起进入一种等待锁释放的状态

自旋锁是一种非阻塞式的锁实现方式、线程在尝试获取锁失败时不会进入阻塞或者挂起状态、而是通过循环自旋的方式等待锁的释放

工作原理是:

  • 当一个线程尝试获取锁、如果锁被占用、线程获取锁失败、但它不会立即进入阻塞或者挂起状态、而是继续尝试获取锁。

  • 这个的自旋的意思就是它会在循环中使用类似 CAS 的方式不断尝试获取锁

  • 在自旋期间、线程保持活跃状态、占用 CPU、不会主动让出。

但是

  • 若锁竞争激烈或临界区耗时长、自旋会导致 CPU 空转、系统吞吐降低

  • 因此在 JDK 或操作系统中、自旋锁常结合自旋一定次数后挂起线程策略使用

参考面试回答

面试官:说一下CAS及底层硬件实现

CAS是一种乐观锁机制、通过比较内存中的值与预期值是否相等来决定是否更新、是实现原子操作的基础。

CAS 操作涉及三个操作数:

  1. 内存位置(V):需要操作的变量的内存地址。

  2. 预期值(A):期望变量当前的值。

  3. 新值(B):希望将变量更新为的新值。

CAS 操作的步骤如下:

  1. 比较内存位置 V 的值是否等于预期值 A。

  2. 如果相等、则将内存位置 V 的值更新为新值 B。

  3. 如果不同、就重试也就是不断的自旋、不断的检查某个变量是否等于期望值直到成功为止。

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实现

http://www.xdnf.cn/news/377137.html

相关文章:

  • Ceph集群故障处理 - PG不一致修复
  • [SV]等待32个instance的某一个信号的pulse,该怎么写?
  • Windows 系统 - Trae 内 终端 无法使用 node (重新配置 nodejs 路径)
  • 青藏高原东北部祁连山地区250m分辨率多年冻土空间分带指数图(2023)
  • AtCoder AT_abc405_d ABC405D - Escape Route
  • 智慧能源大数据平台建设方案(PPT)
  • 数字孪生实战笔记(1)数字孪生的含义、应用及技术体系
  • RPA 浏览器自动化:高效扩展与智能管理的未来
  • SpringBoot学习(上) , SpringBoot项目的创建(IDEA2024版本)
  • 基于阿伦尼斯模型的电池寿命预测:原理、应用与挑战
  • 数据结构:树(树的定义和基本术语)
  • JGL069垃圾填埋场模拟装置试验台
  • 力扣top100 矩阵置零
  • 近日部署跑通的若干多模态模型总结与论文概述
  • clangd与clang-tidy
  • Flutter PIP 插件 ---- 为iOS 重构PipController, Demo界面,更好的体验
  • 优选算法——前缀和
  • Java---StringJoiner 的使用
  • C++11新特性:深入解析decltype关键字及其与auto的区别
  • AI Agent(8):安全与伦理考量
  • [题解]2023CCPC黑龙江省赛 - Folder
  • 警惕C#版本差异多线程中的foreach陷阱
  • 每日c/c++题 备战蓝桥杯(P2241 统计方形(数据加强版))
  • (四)YOLO_World-SAM-GraspNet的mujoco抓取仿真(操作记录)
  • C++STL——priority_queue
  • 运算符与表达式 -《Go语言实战指南》
  • IBM BAW(原BPM升级版)使用教程第八讲
  • 研发效率破局之道阅读总结(5)管理文化
  • 17.【.NET 8 实战--孢子记账--从单体到微服务--转向微服务】--微服务基础工具与技术--ELK
  • Springboot之会话技术