news 2026/10/10 2:40:10

第二章 CAS机制(乐观锁的底层实现,面试核心)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第二章 CAS机制(乐观锁的底层实现,面试核心)

定位:CAS 是乐观锁最典型的实现方式,也是整个JUC并发包的基石;原子类、自旋锁、ConcurrentHashMap都基于它。


1. 什么是 CAS

全称:Compare And Swap,比较并交换,是一条CPU硬件级别的原子指令。

一次CAS操作包含三个要素:

  • V:内存中变量的当前真实值
  • A:线程期望的旧值(线程之前读到的值)
  • B:想要修改成的新值

执行三步(原子完成,不可拆分)

  1. 比较:判断内存当前值V和线程预期旧值A是否相等
  2. 交换:如果相等,把新值B写入内存,返回true(修改成功)
  3. 返回结果:如果不相等,说明数据已被其他线程修改,直接返回false(修改失败)

⚠️ 核心:整个比较+交换是一条CPU原子指令,中途不会被其他线程打断,所以能保证线程安全。

伪代码(仅辅助理解,不是原子)

// 注意:这段Java代码只是逻辑演示,真实CAS由硬件指令完成,本身是原子的 boolean CAS(内存地址, 预期旧值A, 新值B) { if (内存地址当前值 == A) { 内存地址当前值 = B; return true; } return false; }

底层实现链路

Java CAS →Unsafe类的CAS方法 → JVM针对不同系统实现Atomic::cmpxchg→ 汇编CAS指令 + CPU硬件锁机制保证原子性。

一句话:CAS的原子性,最终靠CPU硬件指令支持。

就可以使用CAS完成一些操作,代替“加锁”(多个线程进行锁竞争就很多线程出现阻塞等待,程序跑不快,CAS不涉及阻塞)基于CAS实现的线程安全方式称为“无锁编程”

优点:保证线程安全,同时避免阻塞(效率)

缺点:代码会更复杂,不好理解 ,只能适合一些特定场景,不如枷锁方式更普适

CAS本质是cpu提供的指令—>又被操作系统封装提供成api,又被JVM封装也提供api程序员使用


2. CAS 的两大核心应用

应用一:实现原子类(java.util.concurrent.atomic包)

最典型:AtomicInteger,替代多线程下的i++,线程安全且性能远高于synchronized。

getAndIncrement()等价原子版i++

伪代码实现

class AtomicInteger { private volatile int value; public int getAndIncrement() { int oldValue = value; // CAS失败就循环重试(自旋) while (!CAS(value, oldValue, oldValue + 1)) { oldValue = value; // 重新读取最新值 } return oldValue; } }
双线程执行完整流程(初始value=0)
  1. 线程1、线程2都读取主内存value=0,保存到自己栈的oldValue中。
  2. 线程1先执行CAS:内存值0 == 预期0,修改value为1,返回成功。
  3. 线程2执行CAS:内存值已经是1 ≠ 预期0,返回失败;进入循环,重新读取最新value=1。
  4. 线程2再次CAS:内存值1 == 预期1,修改value为2,返回成功。
  5. 两个线程各自返回旧值,完成一次原子自增。

考点:i++本身不是原子操作(读-改-写三步);CAS通过硬件原子指令,把三步合成一步原子操作。

我用最底层的视角+生活类比,从头把原理刨透,保证每一步都能懂。我们从「为什么i++不安全」开始,一步步推导到CAS、硬件原子指令、Java源码、自旋逻辑,最后完整走一遍双线程并发流程。


第一步:先搞懂——普通int i++为什么一定线程不安全?

先建立内存模型(公告板类比)

我们把主内存想象成一块公共公告板,上面写着数字;每个线程都有自己的工作内存(小本子)。 线程不能直接在公告板上计算,必须:

  1. 读:把公告板的数字抄到自己小本子上
  2. 算:在自己小本子上计算+1
  3. 写:把算好的结果写回公告板

i++本质是三步,不是一步

i++ = 读内存 → 计算+1 → 写回内存

这三步是分开的、非原子的,中间可以被其他线程打断。

并发出错的完整过程(初始i=0,两个线程各加一次)

  1. 线程A、线程B同时抬头看公告板,都抄到了0
  2. 线程A在自己本子上算出1
  3. 线程A把1写回公告板 → 公告板现在是1
  4. 线程B也在自己本子上算出1(因为它抄的还是旧的0)
  5. 线程B把1写回公告板 → 公告板还是1

结果:两个线程各加了一次,本该变成2,结果只变成了1。这就是线程安全问题。

synchronized 怎么解决这个问题?

在公告板上加一把锁,谁拿到锁谁才能「看+算+写」全套做完,其他人排队等。

  • 优点:肯定安全
  • 缺点:人多了都排队,线程挂起唤醒开销大,性能差。

第二步:换个思路——不用锁,能不能解决?(乐观锁思想)

核心想法:我默认没人跟我抢,我直接去改;改的时候核对一下,如果中途没人改过,我就写成功;如果有人改过了,我就重试一次。

对应到公告板例子: 你想把数字从0改成1,不用锁。 走过去先看一眼:“哎?公告板现在还是0吗?”

  • ✅ 还是0:说明没人改过,你直接改成1,完事。
  • ❌ 变成1了:说明有人抢先改了,那你不算了,重新看最新的数字,再算一遍,再核对。

这就是CAS 的核心思想。


第三步:CAS 到底是什么?

全称:Compare And Swap(比较并交换)

一次CAS操作涉及三个值:

  1. V(Value):内存里当前真实的值(公告板上现在的数字)
  2. A(Expect):线程预期的旧值(你之前抄在本子上的数字)
  3. B(New):线程想要改成的新值(你算出来的数字)

执行逻辑(原子完成)

  1. 比较:V 和 A 相等吗?
  2. 相等:把 B 写入内存V,操作成功,返回true
  3. 不相等:什么也不写,操作失败,返回false

最关键的一点:原子性

⚠️「比较+写入」这两个动作,是CPU用一条硬件指令一口气完成的,中间绝对不会被其他线程打断。这不是Java软件实现的,是CPU硬件层面保证的原子性。

类比:你伸手去改公告板,眼睛盯着数字,手同时写,整个动作一气呵成,别人不可能在你核对和书写之间插进来改数字。


第四步:硬件层面——CAS 凭什么能原子?

CPU 提供了专门的原子指令,比如x86架构的cmpxchg指令,这条指令就能完成「比较+交换」的全部操作。 执行这条指令时,CPU会通过总线锁/缓存锁保证这块内存不会被其他CPU同时修改,从而保证原子性。

简单说:这是芯片厂商硬件级别的承诺,不是软件逻辑能模拟的。


第五步:Java 怎么用上硬件的 CAS?

Java 运行在JVM虚拟机上,不能直接调用CPU指令,所以通过下面这条链路逐层调用:

Java代码 → Unsafe类 → native本地方法 → JVM(C++) → 操作系统 → CPU原子指令

1. Unsafe 类

Java专门有个sun.misc.Unsafe类,封装了各种底层内存操作、CAS操作。 因为它操作内存很底层、很危险,所以叫Unsafe,一般不推荐业务代码直接用。

2. native 方法

compareAndSwapInt这个方法被native关键字修饰,叫本地方法。

  • 它不是Java代码写的
  • 它是JVM底层用C++实现的,最终会调用CPU的cmpxchg原子指令

类比:Java是前台接待,Unsafe是内部专员,native是打电话给总部(JVM/C++),总部再找硬件部门(CPU)执行原子操作。


第六步:逐行拆解 AtomicInteger.getAndIncrement() 源码

我们从外到内剥三层。

第一层:AtomicInteger 入口

public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
  • this:当前AtomicInteger对象
  • valueOffset:value字段在内存里的偏移量(直接定位到内存地址)
  • 1:要增加的数值

作用:等价原子版i++,返回自增前的旧值。

第二层:Unsafe.getAndAddInt(核心自旋循环)

public final int getAndAddInt(Object obj, long offset, int delta) { int oldValue; do { // 1. 强制从主内存读最新值 oldValue = getIntVolatile(obj, offset); // 2. 尝试CAS更新;失败就循环重试 } while (!compareAndSwapInt(obj, offset, oldValue, oldValue + delta)); return oldValue; }
逐行翻译成人话
  1. getIntVolatile(obj, offset)volatile读:强制去主内存(公告板)读最新的值,不使用线程自己缓存的旧值。

    保证你拿到的是此时此刻最新的数字。

  2. compareAndSwapInt(obj, offset, oldValue, oldValue+delta)调用native CAS方法,让CPU原子执行: 比较内存当前值 和 预期oldValue → 相等就写入新值,返回成功。
  3. while(...)如果CAS失败了(说明中途有其他线程改了数据),不阻塞、不排队,回到循环开头,重新读最新值,再试一次。

    这就是自旋:线程不停循环重试,像原地打转一样,所以叫自旋锁。

  4. 直到CAS成功,退出循环,返回旧值。

第三层:compareAndSwapInt(native硬件层)

public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
  • native方法,C++实现,最终调用CPU原子指令
  • 整个比较交换是硬件原子的,不会被打断

第七步:双线程并发完整走一遍(初始value=0)

我们把t1、t2两个线程的每一步都拆开,对应公告板例子。

步骤线程t1线程t2主内存(公告板)值
1读取主内存,oldValue=0读取主内存,oldValue=00
2执行CAS:内存0 == 预期0 → 修改为1,成功。退出循环。还没执行CAS1
3返回旧值0,方法结束执行CAS:内存1 ≠ 预期0 → 失败。进入循环体。1
4—重新读取主内存最新值,oldValue=11
5—执行CAS:内存1 == 预期1 → 修改为2,成功。退出循环。2
6—返回旧值1,方法结束2

最终结果:value=2,两个线程各加一次,结果正确。

关键理解

  • CAS失败不是报错,只是告诉你“中途有人改了,你重新来”
  • 自旋循环就是:失败 → 重读最新值 → 再试 → 成功为止
  • 全程没有锁、没有线程挂起,全靠硬件原子指令+软件自旋重试

第八步:几个关键细节刨析

1. 为什么必须用volatile读?

如果不用volatile,线程可能一直读自己缓存的旧值,CAS永远比对不上,死循环。 volatile保证每次读都直接去主内存,拿到最新的值。

2. 自旋锁是乐观锁还是悲观锁?

自旋是等待策略,乐观锁是思想。 CAS+自旋 是典型的乐观锁实现:默认冲突少,直接尝试,失败重试。

3. 为什么CAS比synchronized快?

  • synchronized:抢不到锁 → 线程挂起休眠 → 操作系统唤醒 → 内核态切换,开销极大
  • CAS自旋:抢不到 → 马上重试,线程不挂起,不涉及内核切换,通过重试避免穿插

    但冲突特别多的时候,CAS一直自旋循环,会疯狂消耗CPU,这时候反而重量级锁更合适。


最终总结:AtomicInteger 完整原理一句话

AtomicInteger内部维护一个volatile修饰的int值。自增时进入Unsafe的自旋循环:先volatile读取主内存最新值,再调用native CAS方法由CPU原子执行比较交换;成功则返回旧值,失败则重新读取最新值再次尝试,直到成功。全程基于硬件原子指令,无重量级锁,属于乐观锁实现。


应用二:实现自旋锁

基于CAS可以自己实现轻量级自旋锁,不需要操作系统内核介入。

public class SpinLock { private Thread owner = null; // 记录持有锁的线程 public void lock() { // CAS:期望锁空闲(owner=null),就把owner设为当前线程 while (!CAS(this.owner, null, Thread.currentThread())) { // 抢锁失败,自旋循环重试 } } public void unlock() { owner = null; // 释放锁 } }

原理:抢不到锁就原地循环重试,不挂起线程,属于轻量级锁实现。

在开发中很少会直接使用CAS,但会使用内部封装了CAS的操作


3. CAS 的 ABA 问题(经典面试坑)

什么是ABA问题

线程T1读取变量值为A,准备CAS修改为Z;在T1执行CAS之前,线程T2把变量从A改成B,又改回A。 此时T1执行CAS,发现内存值还是A,就认为数据没被动过,执行修改。

问题:值看起来没变,但中间已经被篡改过一轮,在部分业务场景会引发BUG。

通俗比喻:买手机,你无法区分是全新未拆封,还是别人用过翻新后又变回原样的。

ABA引发的业务BUG

场景:账户余额100元,两个线程并发执行「扣款50元」。

  • 预期:一个成功,一个失败,最终余额50。
异常流程(ABA导致重复扣款)
  1. 初始余额100;线程1、线程2都读到余额=100,准备扣成50。
  2. 线程1先执行CAS,扣款成功,余额变为50。
  3. 线程2执行CAS之前,朋友正好转账50元进来,余额变回100。
  4. 线程2执行CAS:发现余额100 == 预期100,再次扣款成功,余额变成50。

    结果:扣了两次50,总共扣了100,业务逻辑错误。

ABA 解决方案

引入版本号机制:每次修改数据,版本号+1;CAS同时校验「数据值 + 版本号」。

  • 数据值相同、版本号相同 → 判定未修改,执行更新,版本号自增
  • 版本号大于预期 → 判定数据已被修改,操作失败
对应取款例子修复

余额搭配版本号,初始版本=1。

  1. 线程1读到余额100、版本1;CAS成功,余额50,版本变为2。
  2. 朋友转账50,余额变回100,版本变为3。
  3. 线程2执行CAS:余额虽然还是100,但当前版本3 ≠ 预期版本1,操作失败。

    结果:只扣款一次,业务正确。

JDK 内置解决方案

AtomicStampedReference<E>:给对象包装一个版本号戳,内置版本管理,解决ABA问题。


4. 面试高频考点总结

  1. CAS是什么?比较并交换,是CPU硬件原子指令;包含内存值、预期旧值、新值;相等则更新,不相等则失败;全程原子不可拆分。
  2. CAS的应用场景?实现原子类(AtomicInteger等)、实现自旋锁、ConcurrentHashMap优化、轻量级锁底层。
  3. ABA问题是什么,怎么解决?变量值从A改到B又改回A,CAS无法识别中间改动;解决方法是引入版本号,每次修改版本自增,CAS同时校验值和版本号。
  4. CAS是乐观锁还是悲观锁?CAS是乐观锁的典型实现,假设冲突少,直接尝试修改,冲突则失败重试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 2:39:48

连续机制演化下的因果表征学习:方法、实验与工程实践

因果表征学习这几年是越来越热了&#xff0c;但大部分人做的场景都是静态的&#xff1a;环境固定&#xff0c;机制不变&#xff0c;数据一趟学完。可真实世界里几乎没有一成不变的机制——政策会变、设备会老化、用户偏好会漂移。这类非平稳场景里&#xff0c;很多方法还是沿用…

作者头像 李华
网站建设 2026/10/10 2:39:34

拖把更名器免费下载,批量文件命名高效工具下载

拖把更名器是一款专业的文件与文件夹批量重命名工具&#xff0c;支持通过序号、替换、插入、删除、扩展名修改及音乐文件标签提取等多种规则进行重命名。其操作直观、预览实时&#xff0c;适用于需要对大量文件进行系统化、自动化命名整理的摄影、音乐及文档管理场景。拖把更名…

作者头像 李华