← 每日面试题 学习看板

有了CAS为啥还需要volatile?

面试题/00java/02Java 并发编程/04volatile关键字/有了CAS为啥还需要volatile?.md

面试记录 2026-09-26

题目:有了 CAS 为啥还需要 volatile?

难度:medium

标签:java/volatile, general/knowledge

评分:⭐⭐⭐⭐☆

我的回答

CAS 跟 Volatile 不是一个层面的东西,它们处理的事情也不是一个。Volatile 是处理可见性和避免指令重排的,而 CAS 它是一个并发方面的处理方式,它的核心是比较交换,并且在 Java 中,CAS 其实是基于 Volatile 去实现的,二者存在依附关系。因为 CAS 进行比较的时候,需要通过 volatile 去保证可见性,确保比较的对象是当前的值,来最终实现 CAS 比较交换的功能。CAS 跟 Voltaio 相互之间并不是竞争关系,是依附关系。

点评

优点:抓住核心洞察——依附而非竞争关系;正确说明 CAS 基于 volatile 实现;简洁准确

不足:未举 AtomicInteger 源码例子;未展开 DCL 单例为什么两者都要;未说"互补关系"(volatile 管可见性,CAS 管原子性)

补充:依附关系链 volatile 屏障→MESI→CAS 读真实值;互补关系 volatile 管单操作可见、CAS 管复合操作原子;AtomicInteger.value 就是 volatile

完整讲解

第一部分:知识重点剖析

核心考点定位:能否说清"依附 + 互补"两条关系链,而不是把二者对立。

知识点拆解:

  1. 底层依附关系:CAS 在 Java 里就是基于 volatile 实现的。
  1. 互补关系(不是替代关系):
  1. 典型反例:
  1. DCL 单例为什么两者都要:
   private volatile static Singleton s;  // volatile 防止指令重排
   public static Singleton get() {
       if (s == null) {                   // volatile 读,保证可见性
           synchronized (Singleton.class) {
               if (s == null) {           // 二次检查
                   s = new Singleton();   // CAS 或 volatile 写
               }
           }
       }
       return s;
   }
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. i++ 能不能用 volatile?为什么?
  1. CAS 失败后为什么会自旋?自旋期间能看到最新值吗?
  1. Unsafe 和 VarHandle 的 CAS 实现有什么区别?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,CAS 和 volatile 不是竞争关系,而是依附 + 互补的关系,这是我觉得这道题的关键点。

先说依附关系。在 Java 里,CAS 其实就是基于 volatile 实现的。最典型的例子就是 AtomicInteger,它里面的核心字段是 private volatile int value,没有这个 volatile,CAS 根本没法工作。为什么?因为 CAS 要做的是"比较并交换",这个比较动作必须先读到主存的真实值。如果没有 volatile 的内存屏障,JIT 会把值缓存在寄存器里,永远拿不到最新值,CAS 的比较就失去了意义。所以从底层链条看:volatile 写时加 lock 前缀指令 → CPU 触发 MESI 协议让其他核缓存失效 → 下次读必须从主存取 → CAS 才能读到真实值。CAS 是 volatile 的"消费者"。

再说互补关系。volatile 保证的是单条读/写操作的可见性和有序性;CAS 保证的是读-改-写复合操作的原子性。一个管"看",一个管"改",谁也替代不了谁。

最经典的反例是 i++。用 volatile 修饰 i,i++ 仍然不安全,因为 i++ 是三步:读 i、i+1、写回。volatile 只能保证每步操作的可见性,没法保证三步作为一个整体的互斥。这里必须用 AtomicInteger,它的 incrementAndGet 方法内部就是用 CAS 把这三步打包成一个原子操作。反过来,如果只是想读一个值,AtomicInteger.get() 其实就是个 volatile 读,不需要 CAS。

还有一个典型场景是双重检查锁单例,这里 volatile 和 CAS(或 synchronized)必须同时用:

private volatile static Singleton s;
public static Singleton get() {
    if (s == null) {
        synchronized (Singleton.class) {
            if (s == null) {
                s = new Singleton();
            }
        }
    }
    return s;
}

synchronized 保证"判断+赋值"的原子性,但 new 指令会被 JVM 重排成"分配内存→赋引用→初始化构造器",如果不加 volatile,另一个线程可能看到"已经赋引用但还没初始化"的半构造实例,直接用就 NPE。所以 volatile 在这里不是可选的,是必须的。

总结一句话:volatile 是 CAS 的底层基石,CAS 是 volatile 的能力延伸,两者是配合关系不是替代关系。

这就是我对这道题的理解。