深入理解JMM:volatile与CAS是并发安全的两把钥匙

简介: JMM(Java 内存模型)是并发编程的地基。本文用一个死循环 bug 讲透可见性、有序性、原子性三大特性,代码可直接运行,附内存模型与 CAS 示意图。

大家好,我是晚安code。

这篇我按「并发三大特性」的线给你捋:先讲 JMM 定的规矩,再讲 volatile 为什么只管得动两样、CAS 又是怎么把最后一样补上的。代码我基于 JDK 8 跑过(2026 年 8 月实测),你在 JDK 17+ 上结果一样。面试问这块,看完你能接住追问。

一、JMM:并发安全

并发 bug 的根源,几乎都出在「工作内存和主内存之间拷贝不及时」这八个字上。

JMM(Java 内存模型,Java Memory Model):Java 虚拟机规范里约定的一套内存规则,规定线程怎么读写共享变量、什么时候能看到最新值。你可以把它理解成多线程世界的「交通规则」,不守规矩就要撞车。

JMM 把内存拆成两块:

  • 主内存:所有共享变量都住在这里,所有线程共享这一份。
  • 工作内存:每个线程一块独享的副本,线程只能在这块副本上动手,不能直接碰主内存。

所以两个线程之间的值传递,永远得绕主内存走一圈。绕得慢,就会读到旧值。

围绕这套「主内存 ⇄ 工作内存」,JMM 还约定了三条同步规则,这也是 synchronized 的语义来源:

  1. 线程解锁前,必须把共享变量的新值刷回主内存。
  2. 线程加锁前,必须从主内存把最新值读回工作内存。
  3. 加锁和解锁得是同一把锁。

图 1 就是主内存和工作内存的关系:两个线程各抱各的副本,唯一的通道就是主内存,线程之间谁也看不见谁的副本。

JMM 内存模型:主内存与两个线程工作内存之间通过 read/load、store/write 同步,线程间看不见对方副本

二、特性①可见性:volatile 的可见性靠什么保证

一个变量不加 volatile,多线程下你可能读到「过期值」——这是并发面试的第一道送分题。

public class StopThreadDemo {
   
    // 去掉 volatile,这个程序会一直空转,停不下来
    private static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {
   
        Thread worker = new Thread(() -> {
   
            while (running) {
    /* 模拟干活 */ }
        });
        worker.start();

        Thread.sleep(1000);
        running = false;   // 主线程改标记,想让 worker 停下
        System.out.println("main 已把 running 改为 false");
    }
}

worker 看不见这个改动,是因为它工作内存里的 running 副本还是 true。volatile 干的事就两件:写的时候强制把新值刷回主内存,读的时候强制从主内存重新拿。加上它,这个程序才能正常退出。

volatile 最典型的用法就是「状态开关」:一个线程改、其它线程读。这类「一写多读」场景用它最划算,不加锁也不阻塞。

volatile 就是那座架起鸿沟的桥——写回主内存、读自主内存

三、特性②有序性:指令重排与内存屏障

volatile 的第二个能力是禁止指令重排,底层靠内存屏障实现——相当于给代码排队装了两道铁栏杆。

你写的代码,编译器和 CPU 为了提速会偷偷调换执行顺序。单线程无所谓(结果一样),多线程就可能出事。看这个经典例子,a、b、x、y 初始都是 0:

// 线程A:        // 线程B:
x = a;            y = b;
b = 1;            a = 2;

正常跑下来 x=0、y=0。但如果两边都重排成「先写再读」:

// 线程A 重排后:  // 线程B 重排后:
b = 1;            a = 2;
x = a;            y = b;

就可能出现 x=2、y=1 这种按顺序执行永远到不了的怪结果。

内存屏障(Memory Barrier):一条 CPU 指令,作用有两个——保证屏障前后的指令顺序不被调换,顺便强制内存可见性。volatile 的读写前后都会插屏障,把重排的路堵死。

屏障两侧的指令不能互相越界,顺序就稳住了

可能有人会问:DCL 单例为什么必须加 volatile?

instance = new Singleton() 看着是一行,其实有「分配内存 → 初始化对象 → 指向引用」三步,不加 volatile 可能被重排成先指向引用再初始化,别的线程就会拿到一个半成品。volatile 禁止重排,保证对象完整发布后再被别人看到。

四、特性③原子性:volatile 不保证原子性,CAS 来补

volatile 不保证原子性——num++ 压根不是一条指令,是「读-改-写」三步,中间随时可能被插队。

public class AtomicityDemo {
   
    private static volatile int count = 0;

    static void add() {
    count++; }   // 读 -> 改 -> 写 三步

    public static void main(String[] args) throws InterruptedException {
   
        for (int i = 0; i < 20; i++) {
   
            new Thread(() -> {
   
                for (int j = 0; j < 1000; j++) add();
            }).start();
        }
        Thread.sleep(3000);   // 简化写法:睡到所有线程跑完
        System.out.println("count = " + count);   // 永远不是 20000
    }
}

我第一次跑这个,count 出来是 18938,怎么都想不通为啥不是 2 万。后来才明白:volatile 只保证「看得见最新值」,不保证「读写这个过程不被切开」。两个线程同一瞬间都读到 count=5,都加 1 再写回,就互相覆盖了一次。

缺口怎么补?两条路:加锁,或者用原子类。

CAS(Compare And Swap,比较并交换):一种无锁的原子操作,只有当前内存值等于我预期的旧值时,才把新值写进去;不等就重试。你可以把它理解成「乐观锁」——先相信没人抢,动手前再验一次货。

把上面 demo 换一行就能修好:

AtomicInteger count = new AtomicInteger(0);
count.getAndIncrement();   // 原子 +1,底层就是 CAS

同样 20 线程跑完,结果稳定 20000。

CAS 的底层是 Unsafe 类。Unsafe 类:Java 留的一个能直接操作内存的「后门」,CAS 的 native 方法都是它提供的。AtomicInteger 的 getAndIncrement 一路调到 Unsafe,核心逻辑就是自旋——比不对就一直重读再比:

// Unsafe.getAndAddInt 简化逻辑
public final int getAndAddInt(Object o, long offset, int delta) {
   
    int v;
    do {
   
        v = getIntVolatile(o, offset);                    // 1. 读当前值
    } while (!compareAndSwapInt(o, offset, v, v + delta)); // 2. 比得上就写,比不上继续转
    return v;
}

CAS 的「转圈」长图 6 这样:读 → 比 → 不行就重读,直到成功。

CAS 自旋流程:读当前值、与预期值比较,不相等就重新读继续尝试,相等才写入新值

五、CAS 的坑:ABA 问题与原子引用

CAS 有个天然盲区叫 ABA 问题——值绕一圈回到原点,CAS 就以为没人动过,其实中间已经悄悄变了两次。

ABA 问题:线程 1 读到一个值 A,线程 2 把它改成 B 又改回 A,线程 1 拿手里的 A 去比对,发现还是 A,直接更新成功——可中间其实被改过两回了。这就是成语里的「狸猫换太子」。

你品一下这个场景:收货时验货单上写着「货:A」,你验完满意签收。可快递员中途把货换成 B 又换回 A 了——你签收的确实是 A,但中途确实被掉过包。这种「看着没变、实际动过」的情况,CAS 完全看不出来。

解决办法是给值配个版本号,CAS 时值和版本号一起比:

public class AbaFixDemo {
   
    // 值 + 版本号(stamp) 一起管
    static AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(2020, 0);

    public static void main(String[] args) {
   
        int[] stamp = new int[1];
        Integer cur = ref.get(stamp);   // 一次拿回「值 + 版本号」

        // 别的线程:2020->2021->2020,版本号 0->1->2
        ref.compareAndSet(2020, 2021, 0, 1);
        ref.compareAndSet(2021, 2020, 1, 2);

        // 我们期望版本还是 0,比对失败,ABA 被拦下
        System.out.println(ref.compareAndSet(2020, 6666, 0, 3));  // false
    }
}

AtomicStampedReference(原子引用):给引用值配一个版本号戳,CAS 时值和版本号都得匹配才更新,用来识别「先变后恢复」的过程变化。你可以理解为「验货时不但看货,还看货单上写的是第几版」。

只比对值,认不出被调包;加上版本号戳,一眼识破

用它有个坑:别分开调 getReference() 和 getStamp(),两次调用之间状态可能已经变了,要像我上面这样用 get(stampHolder) 一次把值和版本号都拿回来。

可能有人会问:CAS 自旋一直失败会怎样?

会一直空转烧 CPU。自旋是拿 CPU 换等待,竞争激烈时线程一多,一圈圈空转反而更慢,这时候该退回悲观锁(synchronized / Lock)。

六、总结:一张表看清 volatile、CAS、synchronized

volatile、CAS、synchronized 不是三选一,是三个维度各管一摊——能分清谁管什么,面试问并发就不心虚。

特性 volatile CAS(原子类) synchronized
原子性 不保证 保证(单变量) 保证
可见性 保证 保证(靠底层 volatile) 保证
有序性 保证(禁重排) 部分 保证
代价 无阻塞 自旋可能空转 线程阻塞

一句话收束:并发安全 = 可见性 + 有序性 + 原子性,JMM 定规则,volatile 管前两样,CAS 补最后一样。下次再被问「谈谈你对 volatile 的理解」,先抛这个框架再往下拆,稳的。


我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:学并发的时候,你是先啃 JMM 还是先背 CAS?有没有在哪一步卡了很久?

目录
相关文章
|
9天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1325 11
|
14天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1960 9
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
8天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
545 112
缓存 安全 IDE
1267 2
|
20天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
3080 4
|
8天前
|
人工智能 JSON Shell
2026AI漫剧本地全开源方案(附各个软件模型链接),8G显卡也能流畅运行
这是一套完全本地化部署的AI漫剧生成技术链路:涵盖LLM剧本分镜生成、FLUX文生图(IP-Adapter人脸锁定)、StoryDiffusion时序连贯控制、LTX-2.3唇形同步视频生成,及ComfyUI全流程调度。零云端费用,仅耗硬件算力,单集2–4小时可产出竖屏短视频,适配抖音/B站分发。
|
6天前
|
编解码 弹性计算 云计算
MiniMax-H3 视频生成模型 — 一键部署与使用指南
MiniMax-H3是MiniMax开源的33B全模态视频生成模型,支持文生视频、图生视频、参考生视频三种模式,原生输出2K/15秒带立体声音频视频,已原生适配ComfyUI,并可通过阿里云计算巢一键部署。(239字)
|
13天前
|
存储 人工智能 关系型数据库
阿里云AI产品与云产品最新组合套餐:Token Plan、AI coding及云服务器和建站等组合优惠价
阿里云推出全新“算力+模型+应用”一站式云与AI组合套餐活动,覆盖从个人开发者到中大型企业的全场景需求。核心亮点为分三档定价的Token Plan订阅服务,支持Qwen3.8-Max-Preview大模型调用,错峰时段最低可享0.2折优惠。活动同步推出AI Coding、智能体部署、云电脑托管、0代码建站等十余类场景化组合,搭配99元/年的普惠云服务器、88元/年的入门数据库等经典特惠产品,还为企业提供1V1定制化AI转型方案,大幅降低了不同用户群体拥抱AI的技术门槛与采购成本。
757 111

热门文章

最新文章