JMM、volatile 与 CAS:可见性/有序性/原子性、ABA 与性能取舍
·
并发里最容易“背会但不会用”的,就是 JMM、volatile 和 CAS。
你要能把它们串成一条主线:
- 并发正确性三要素:可见性、有序性、原子性
- JMM 为什么需要 happens-before
- volatile 到底保证了什么、不保证什么
- CAS 为什么能做无锁、它的坑是什么(ABA、自旋开销)
如果你只想记一句工程结论:
- volatile 解决“看不见/顺序错”,CAS/锁解决“原子性”。
1. 并发正确性三要素
- 可见性:一个线程修改变量,另一个线程能及时看到
- 有序性:程序执行顺序是否符合你预期(重排序问题)
- 原子性:操作不可被拆分/中断
一个非常重要的点:
- 很多 bug 不是“值算错了”,而是“值更新看不到/看到了旧值”。
2. JMM(Java Memory Model)解决什么矛盾
矛盾是:
- CPU/编译器为了性能会做缓存与重排序
- 但并发程序需要一定的可见性与顺序保证
JMM 用一套规则(happens-before)来定义:
- 什么情况下,一个线程的写对另一个线程可见
3. happens-before:你不需要背全,但要会用
常用的几条(工程与面试够用):
- 程序次序规则:同线程内,前面的操作 happens-before 后面的操作
- volatile 规则:对 volatile 的写 happens-before 后续对同一 volatile 的读
- 监视器锁规则:unlock happens-before 后续对同一锁的 lock
- 线程启动/终止规则:start/join 的可见性保证
你要会用它解释:
- 为什么加锁能保证可见性
- 为什么 volatile 写后其它线程能读到
4. volatile:保证什么,不保证什么
4.1 volatile 保证:可见性 + 有序性(禁止特定重排序)
- 写 volatile 会把工作线程的修改刷新出去
- 读 volatile 会让后续读取看到最新值
工程上你可以这样解释 volatile 的“有序性”:
- 它会在读写位置建立内存语义,禁止把关键读写重排序到 volatile 读写两侧
- 这让“发布-订阅”(先写对象,再写 flag;读 flag 后再读对象)变得成立
4.2 volatile 不保证:复合操作的原子性
经典反例:
count++不是原子操作(读-改-写)
所以:
- volatile 适合做开关(stop flag)、状态发布
- 计数器这类需要原子性,优先用
AtomicInteger或锁
5. DCL(双重检查锁)为什么必须配 volatile
单例的双重检查锁(DCL)是 volatile 最经典的落地场景之一。
正确写法的关键点:
instance必须是 volatile,避免“半初始化对象”被别的线程看到
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
6. CAS:无锁原子更新的核心
CAS(Compare And Swap)思想:
- 比较内存中的值是否等于期望值
- 相等则更新为新值(原子)
你在 Java 里常见的落地:
AtomicInteger、LongAdder- AQS 的 state 更新
7. CAS 的坑:ABA + 自旋开销
7.1 ABA 问题
问题描述:
- 线程看到值是 A
- 期间值变成 B
- 又变回 A
- CAS 仍然成功,但中间变化被“忽略”
解决思路:
- 版本号(Stamped)
AtomicStampedReference
7.2 自旋开销
CAS 失败会重试:
- 竞争激烈时会大量自旋,CPU 飙高
工程结论:
- CAS 适合低冲突、短临界区
- 冲突高时,锁/队列阻塞可能更划算
8. AtomicInteger vs LongAdder:高并发计数怎么选
AtomicInteger:单点 CAS 更新,高冲突下重试多LongAdder:分段累加(热点拆分),高并发下吞吐更高
工程结论:
- 低并发/需要强一致读取:AtomicInteger 足够
- 高并发热点计数:LongAdder 更合适
9. volatile 与 CAS 的组合:典型模式
- volatile 做状态发布(例如初始化完成标记)
- CAS 做计数/更新
常见的正确姿势:
- 读多写少:volatile + 不可变对象
- 计数高并发:LongAdder
10. 一个最小复现:为什么不加 volatile 会卡死
public class StopFlag {
static boolean stop = false;
public static void main(String[] args) throws Exception {
new Thread(() -> {
while (!stop) {
// busy
}
System.out.println("stopped");
}).start();
Thread.sleep(1000);
stop = true;
}
}
这个例子在某些情况下可能无法停止,因为 stop 的更新对另一个线程不可见。
修复:
- 把
stop改成volatile,或者用锁/原子类建立 happens-before。
11. 线上排查:怎么判断是可见性/重排序问题
典型信号:
- 低概率复现、加日志后“好了”(时序变化)
- 只在多核/高并发下出现
排查建议:
- 先用 happens-before 规则审视:写和读之间是否有同步关系
- 如果没有同步关系:加 volatile / 锁 / 原子类
排查清单(实战更好用):
- 共享变量是否有“发布”动作(volatile 写/锁释放)
- 读线程是否有“获取”动作(volatile 读/锁获取)
- 是否存在复合操作(++、check-then-act)却只用了 volatile
12. 面试追问 Q&A(高频)
- Q:volatile 能保证原子性吗?
- A:不能,它只保证可见性与部分有序性,
++仍然是读改写。
- A:不能,它只保证可见性与部分有序性,
- Q:为什么 DCL 必须 volatile?
- A:防止对象创建过程重排序,导致别的线程读到半初始化对象。
- Q:CAS 为什么会 CPU 高?
- A:竞争激烈时 CAS 失败重试,自旋占用 CPU。
13. 面试表达(30 秒讲清楚)
- 并发正确性看可见性、有序性、原子性。
- JMM 用 happens-before 定义跨线程可见性与顺序保证。
- volatile 保证可见性并禁止部分重排序,但不保证
++这类复合操作的原子性。 - CAS 通过比较交换提供无锁原子更新,适合低冲突场景;高冲突会自旋浪费 CPU,并且存在 ABA 问题,可用带版本号的引用解决。
14. 总结
- volatile:状态发布/开关很合适,计数不行
- CAS:低冲突高性能,但要注意 ABA 与自旋成本
- 并发 bug 排查:先看 happens-before 是否成立
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)