定位:CAS 是乐观锁最典型的实现方式,也是整个JUC并发包的基石;原子类、自旋锁、ConcurrentHashMap都基于它。
1. 什么是 CAS
全称:Compare And Swap,比较并交换,是一条CPU硬件级别的原子指令。
一次CAS操作包含三个要素:
V:内存中变量的当前真实值A:线程期望的旧值(线程之前读到的值)B:想要修改成的新值
执行三步(原子完成,不可拆分)
- 比较:判断内存当前值
V和线程预期旧值A是否相等 - 交换:如果相等,把新值
B写入内存,返回true(修改成功) - 返回结果:如果不相等,说明数据已被其他线程修改,直接返回
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、线程2都读取主内存
value=0,保存到自己栈的oldValue中。 - 线程1先执行CAS:内存值0 == 预期0,修改value为1,返回成功。
- 线程2执行CAS:内存值已经是1 ≠ 预期0,返回失败;进入循环,重新读取最新value=1。
- 线程2再次CAS:内存值1 == 预期1,修改value为2,返回成功。
- 两个线程各自返回旧值,完成一次原子自增。
考点:
i++本身不是原子操作(读-改-写三步);CAS通过硬件原子指令,把三步合成一步原子操作。
我用最底层的视角+生活类比,从头把原理刨透,保证每一步都能懂。我们从「为什么i++不安全」开始,一步步推导到CAS、硬件原子指令、Java源码、自旋逻辑,最后完整走一遍双线程并发流程。
第一步:先搞懂——普通int i++为什么一定线程不安全?
先建立内存模型(公告板类比)
我们把主内存想象成一块公共公告板,上面写着数字;每个线程都有自己的工作内存(小本子)。 线程不能直接在公告板上计算,必须:
- 读:把公告板的数字抄到自己小本子上
- 算:在自己小本子上计算+1
- 写:把算好的结果写回公告板
i++本质是三步,不是一步
i++ = 读内存 → 计算+1 → 写回内存这三步是分开的、非原子的,中间可以被其他线程打断。
并发出错的完整过程(初始i=0,两个线程各加一次)
- 线程A、线程B同时抬头看公告板,都抄到了
0 - 线程A在自己本子上算出
1 - 线程A把
1写回公告板 → 公告板现在是1 - 线程B也在自己本子上算出
1(因为它抄的还是旧的0) - 线程B把
1写回公告板 → 公告板还是1
结果:两个线程各加了一次,本该变成2,结果只变成了1。这就是线程安全问题。
synchronized 怎么解决这个问题?
在公告板上加一把锁,谁拿到锁谁才能「看+算+写」全套做完,其他人排队等。
- 优点:肯定安全
- 缺点:人多了都排队,线程挂起唤醒开销大,性能差。
第二步:换个思路——不用锁,能不能解决?(乐观锁思想)
核心想法:我默认没人跟我抢,我直接去改;改的时候核对一下,如果中途没人改过,我就写成功;如果有人改过了,我就重试一次。
对应到公告板例子: 你想把数字从0改成1,不用锁。 走过去先看一眼:“哎?公告板现在还是0吗?”
- ✅ 还是0:说明没人改过,你直接改成1,完事。
- ❌ 变成1了:说明有人抢先改了,那你不算了,重新看最新的数字,再算一遍,再核对。
这就是CAS 的核心思想。
第三步:CAS 到底是什么?
全称:Compare And Swap(比较并交换)
一次CAS操作涉及三个值:
- V(Value):内存里当前真实的值(公告板上现在的数字)
- A(Expect):线程预期的旧值(你之前抄在本子上的数字)
- B(New):线程想要改成的新值(你算出来的数字)
执行逻辑(原子完成)
- 比较:V 和 A 相等吗?
- 相等:把 B 写入内存V,操作成功,返回true
- 不相等:什么也不写,操作失败,返回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; }逐行翻译成人话
getIntVolatile(obj, offset)volatile读:强制去主内存(公告板)读最新的值,不使用线程自己缓存的旧值。保证你拿到的是此时此刻最新的数字。
compareAndSwapInt(obj, offset, oldValue, oldValue+delta)调用native CAS方法,让CPU原子执行: 比较内存当前值 和 预期oldValue → 相等就写入新值,返回成功。while(...)如果CAS失败了(说明中途有其他线程改了数据),不阻塞、不排队,回到循环开头,重新读最新值,再试一次。这就是自旋:线程不停循环重试,像原地打转一样,所以叫自旋锁。
- 直到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=0 | 0 |
| 2 | 执行CAS:内存0 == 预期0 → 修改为1,成功。退出循环。 | 还没执行CAS | 1 |
| 3 | 返回旧值0,方法结束 | 执行CAS:内存1 ≠ 预期0 → 失败。进入循环体。 | 1 |
| 4 | — | 重新读取主内存最新值,oldValue=1 | 1 |
| 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导致重复扣款)
- 初始余额100;线程1、线程2都读到余额=100,准备扣成50。
- 线程1先执行CAS,扣款成功,余额变为50。
- 线程2执行CAS之前,朋友正好转账50元进来,余额变回100。
- 线程2执行CAS:发现余额100 == 预期100,再次扣款成功,余额变成50。
结果:扣了两次50,总共扣了100,业务逻辑错误。
ABA 解决方案
引入版本号机制:每次修改数据,版本号+1;CAS同时校验「数据值 + 版本号」。
- 数据值相同、版本号相同 → 判定未修改,执行更新,版本号自增
- 版本号大于预期 → 判定数据已被修改,操作失败
对应取款例子修复
余额搭配版本号,初始版本=1。
- 线程1读到余额100、版本1;CAS成功,余额50,版本变为2。
- 朋友转账50,余额变回100,版本变为3。
- 线程2执行CAS:余额虽然还是100,但当前版本3 ≠ 预期版本1,操作失败。
结果:只扣款一次,业务正确。
JDK 内置解决方案
AtomicStampedReference<E>:给对象包装一个版本号戳,内置版本管理,解决ABA问题。
4. 面试高频考点总结
- CAS是什么?比较并交换,是CPU硬件原子指令;包含内存值、预期旧值、新值;相等则更新,不相等则失败;全程原子不可拆分。
- CAS的应用场景?实现原子类(AtomicInteger等)、实现自旋锁、ConcurrentHashMap优化、轻量级锁底层。
- ABA问题是什么,怎么解决?变量值从A改到B又改回A,CAS无法识别中间改动;解决方法是引入版本号,每次修改版本自增,CAS同时校验值和版本号。
- CAS是乐观锁还是悲观锁?CAS是乐观锁的典型实现,假设冲突少,直接尝试修改,冲突则失败重试。