CAS(Compare And Swap)为什么能保证线程安全? ⭐⭐⭐⭐⭐
面试频率:★★★★★ 工作频率:★★★★★
⚡30 秒速记(复习只看这里)
🟢 一句话
CAS(Compare And Swap)是一种乐观锁机制,通过比较内存中的旧值是否发生变化,如果没有变化则更新,否则不断重试。
🟡 CAS 三个核心参数
当前值
期望值
新值执行:
如果当前值 == 期望值
↓
替换成新值否则:
失败🟠 CAS 核心思想
不是:
先加锁
再修改而是:
先尝试修改
失败再重试所以叫:
乐观锁。
🔴 CAS 解决什么问题?
解决:
多个线程同时修改共享变量导致的数据覆盖问题。
📚 完整知识
一、为什么需要 CAS?
看普通代码:
count++;很多人认为:
一步完成。
实际上:
读取 count
↓
count + 1
↓
写回 count包含多个步骤。
两个线程:
初始:
count = 0线程A:
读取 count=0线程B:
读取 count=0线程A:
写入1线程B:
写入1最终:
count=1但是:
正确结果应该:
count=2这就是:
数据丢失。
二、synchronized 如何解决?
加锁:
synchronized(lock){
count++;
}流程:
线程A
↓
获取锁
↓
修改数据
↓
释放锁
线程B
↓
等待
↓
获取锁
↓
修改数据安全。
但是:
问题:
竞争高时:
线程阻塞
线程切换
性能下降三、CAS 怎么解决?
CAS:
包含三个值:
内存当前值
期望值
新值例如:
当前:
count = 0线程A:
希望:
0 → 1执行:
CAS(当前值,期望值,新值)
CAS(0,0,1)意思:
如果现在还是0,就修改成1。
成功:
0 → 1返回:
true如果其他线程已经修改:
例如:
count=1线程A:
再次执行:
CAS(1,0,1)发现:
当前值不是0失败。
然后重新读取。
四、为什么 CAS 是线程安全的?
因为:
CAS 是 CPU 提供的原子操作。
比较和交换:
是一个不可分割的整体。
不会出现:
比较成功
↓
被其他线程修改
↓
再交换这种情况。
五、CAS 底层原理
Java 中:
CAS 最终依赖:
CPU 原子指令JVM:
通过:
Unsafe调用底层 CAS。
例如:
AtomicInteger:
AtomicInteger count =
new AtomicInteger(0);调用:
count.incrementAndGet();底层类似:
while(true){
oldValue = 当前值;
newValue = oldValue + 1;
if(CAS(oldValue,newValue)){
break;
}
}这个过程叫:
自旋。
六、什么是自旋?
CAS 失败后:
不是等待。
而是:
重新尝试。
例如:
CAS失败
↓
再次CAS
↓
再次失败
↓
继续尝试优点:
避免线程阻塞。
缺点:
消耗 CPU。
七、CAS 的问题
1. 高竞争导致 CPU 消耗
例如:
100个线程:
同时修改:
count结果:
一个成功
99个失败失败线程:
不断:
读取
↓
CAS
↓
失败
↓
重试CPU压力增加。
2. ABA 问题 ⭐⭐⭐⭐⭐
这是面试重点。
假设:
初始:
A线程A:
读取:
A准备修改。
线程B:
修改:
A → B然后:
B → A线程A回来:
看到:
还是A认为:
没有变化。
继续修改。
但是实际上:
中间已经发生变化。
这就是:
ABA问题。
八、如何解决 ABA?
增加版本号。
普通:
A改成:
A(version=1)变化:
A1
↓
B2
↓
A3线程回来:
发现:
A3 != A1知道中间发生过变化。
Java:
提供:
AtomicStampedReference解决 ABA。
九、CAS 和 synchronized 区别
十、CAS 和 volatile 区别
很多人容易混淆。
volatile
解决:
可见性例如:
volatile boolean flag;一个线程修改:
其他线程能看到。
CAS
解决:
修改过程原子性例如:
count++;保证:
不会被覆盖。
所以:
volatile
↓
保证看见
CAS
↓
保证修改成功十一、为什么 ConcurrentHashMap 使用 CAS?
JDK8:
ConcurrentHashMap:
CAS
+
synchronized桶为空:
例如:
table[5] == null只需要:
null → Node修改一个引用。
适合 CAS。
桶有数据:
例如:
Node
↓
Node需要修改:
next指针
节点关系
红黑树结构多个操作。
不适合 CAS。
使用:
synchronized十二、AtomicInteger 为什么线程安全?
代码:
AtomicInteger count =
new AtomicInteger();执行:
count.incrementAndGet();内部:
读取旧值
↓
计算新值
↓
CAS更新
↓
失败重新尝试所以:
多线程环境安全。
💼 工作场景
计数器
错误:
private int count;
count++;多线程:
可能数据丢失。
正确:
AtomicInteger count =
new AtomicInteger();
count.incrementAndGet();🎤 面试回答(30秒)
CAS 是一种乐观锁机制,通过比较内存中的旧值和期望值,如果一致则更新为新值,否则重新尝试。CAS 依赖 CPU 原子指令,因此能够保证单个变量修改的原子性。Java 中 AtomicInteger、ConcurrentHashMap 等底层都使用 CAS。但是 CAS 存在 ABA 问题和高竞争导致 CPU 消耗的问题,因此复杂场景通常结合 synchronized 使用。
🎯 面试追问
Q1:CAS 是锁吗?
答:
不是传统意义上的锁。
CAS 是一种无锁的乐观同步机制。
Q2:CAS 为什么线程安全?
答:
因为比较和交换是 CPU 原子操作,不会被线程打断。
Q3:CAS 有什么缺点?
答:
高竞争时大量自旋消耗 CPU。
存在 ABA 问题。
Q4:ABA 怎么解决?
答:
增加版本号,例如 AtomicStampedReference。
Q5:CAS 和 synchronized 怎么选择?
答:
简单变量修改使用 CAS。
复杂业务逻辑使用 synchronized。
Q6:ConcurrentHashMap 为什么 CAS 和 synchronized 一起用?
答:
空桶初始化使用 CAS。
复杂结构修改使用 synchronized。
⚠️ 工作踩坑
1. 认为 CAS 适合所有场景
错误。
链表、红黑树等复杂结构不适合。
2. 认为 CAS 没有锁所以一定更快
错误。
高竞争下大量自旋会消耗 CPU。
3. 认为 volatile 可以代替 CAS
错误。
volatile 只保证可见性。
❓ 自测
CAS 是什么?
CAS 三个参数是什么?
CAS 为什么线程安全?
什么是自旋?
CAS 有什么缺点?
什么是 ABA 问题?
如何解决 ABA?
AtomicInteger 为什么线程安全?
ConcurrentHashMap 为什么同时使用 CAS 和 synchronized?