并发里最容易“背会但不会用”的,就是 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 里常见的落地:

  • AtomicIntegerLongAdder
  • 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:不能,它只保证可见性与部分有序性,++ 仍然是读改写。
  • 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 是否成立
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐