当前位置: 首页 > news >正文

Java 并发编程图鉴:一口气讲清 volatile 的底层原理

说起 volatile,很多人第一反应就是“能保证多线程下的可见性”,再问深一点就支支吾吾了。刚好最近在翻 JMM 的底层逻辑,索性一口气把 volatile 的裤子扒干净,连字节码和汇编都给你掏出来看看。

先看一段让人头疼的代码:

public class NoVisibility {private static boolean ready;private static int number;private static class ReaderThread extends Thread {public void run() {while (!ready) {Thread.yield();}System.out.println(number);}}public static void main(String[] args) throws InterruptedException {new ReaderThread().start();number = 42;ready = true;}
}

这段代码有意思的地方在于:ReaderThread 可能会一直死循环,永远看不到 ready 变成 true;就算看到了,打印出来的 number 也可能是 0,而不是 42。这就是典型的可见性和重排序问题。主线程那边 number = 42 和 ready = true 这两行代码,在 CPU 眼里是没有依赖关系的,它完全可以先改 ready 再改 number,而 ReaderThread 看到的顺序又可能跟写入顺序不一致。

给 ready 加上 volatile 之后:

private static volatile boolean ready;

神奇的事情发生了——循环能退出了,number 也稳如老狗地输出 42。volatile 在这里干了三件事:一是强制把修改立即刷回主内存,二是让其他线程读这个变量时直接从主内存拿,三是禁止指令重排序。但这三个描述只是表象,底层逻辑要硬核得多。

回过头看另一个经典场景,双重检查锁定的单例:

public class Singleton {private static volatile Singleton instance; // 必须有 volatilepublic static Singleton getInstance() {if (instance == null) {                 // 第一次检查synchronized (Singleton.class) {if (instance == null) {         // 第二次检查instance = new Singleton(); // 问题就在这一行}}}return instance;}
}

如果不加 volatile,instance = new Singleton() 这一行在字节码层面可以拆成三步:

  1. 分配内存空间
  2. 初始化对象(调用构造方法)
  3. 将引用赋值给 instance

但 CPU 可能会把步骤 2 和 3 重排,变成先给 instance 赋值(此时 instance 非空),再初始化对象。这时另一个线程在第一次检查时发现 instance 不为 null,直接拿去用,拿到的却是个半成品对象,业务可能就崩了。

这就是 volatile 禁止指令重排序的意义——它给变量的写操作前后加上了“内存屏障”。具体到 JVM 的实现,volatile 写操作前面会插入 StoreStore 屏障,后面插入 StoreLoad 屏障;读操作后面会插入 LoadLoad 和 LoadStore 屏障。这些屏障就像一堵墙,强行把指令的乱序执行给框死。

光讲抽象的屏障还是不够过瘾,我们把生成的汇编代码扒出来看看。在 JIT 编译后的代码里,对 volatile 变量的写操作(比如 ready = true),在 x86 架构上会翻译成带 lock 前缀的指令:

movb $0x1,0x70(%rsi)  ; 普通写
lock addl $0x0,(%rsp) ; 这就是内存屏障的体现

这个 lock 前缀太关键了,它的作用比单纯加锁要底层得多:它会锁住总线或者使用缓存一致性协议(比如 MESI),让当前 CPU 缓存行的修改立即写回主内存,同时让其他 CPU 里对应的缓存行失效。这样一来,其他线程再读这个变量的时候,CPU 发现缓存行失效,就只能乖乖去主内存重新加载,可见性就是这么硬扛出来的。

而且 lock 指令本身也相当于一个“万能屏障”——在它之前的读写操作一定不会被重排到它之后,反之亦然。所以 JIT 编译器在遇到 volatile 时,会根据不同平台生成对应的屏障指令,x86 平台上 StoreLoad 屏障就是靠 lock 前缀完成的,而 ARM 架构则需要显式地插入 DMB 指令。

说到这儿你可能会觉得 volatile 挺牛逼的,顺手就想用它来解决累加问题,比如:

private static volatile int count = 0;
// 十个线程各加 1000 次

结果跑出来永远小于 10000,然后你满头问号。因为 volatile 不保证原子性,count++ 这个操作读和写是两个步骤,中间被其他线程插一杠子就丢失更新了。这个坑我当年踩过,还以为是 Java 的 bug。

所以 volatile 适用场景其实很明确:一个线程写,多个线程读,或者作为状态标志位;还有就是前面说的双重检查锁定,用它来防止对象逸出。一旦涉及“先检查后执行”的复合操作,老老实实上锁或者用 Atomic 类吧。

这样一路刨下来,你会发现 volatile 的底层就是靠着 JMM 的 happens-before 规则、内存屏障、CPU 的 lock 指令和缓存一致性协议这一整套组合拳,才把并发编程里最容易出鬼的可见性和重排序问题给按住。面试的时候如果能把这一串串起来讲,比背八股文有说服力多了。

http://www.jsqmd.com/news/970439/

相关文章:

  • 花一天排查出的线上 GC 频繁问题,竟是因为一个配置
  • Node-RED Dashboard终极指南:3步打造专业级可视化界面
  • 2026温州GEO优化公司深度评测与选型避坑指南 - 品牌报告
  • 台州云栖雅筑(宸智雅筑)装饰官方联系方式 合作电话 官网入口 避坑指南 - 资讯纵览
  • openLCA 2.6.2 终极指南:免费开源的生命周期评估解决方案
  • Node.js版本太低?手把手教你用NVM切换版本,解决NPM安装时的EUNSUPPORTEDPROTOCOL错误
  • 5步掌握LinkSwift:告别网盘限速的终极下载指南
  • Markdown文档和工具
  • 用数据说话!盘点2026年学生热捧的的降AIGC软件 - 降AI小能手
  • Linux内核学习轨迹第五部:页缓存Page Cache与回写机制(第九小节)
  • 前端初学者如何深度理解 如何创建一个路由页面
  • 【Android】 VidFetch一键下载各大平台视-内置播放器
  • PI XLs Designer v8.0:电源变压器设计的精密计算与深度优化指南
  • 2026荔湾区搬家公司终极评测排行|全域覆盖、价格透明、安全保障深度实测避坑指南 - gzdjxd
  • MonkeyCode从入门到精通:完整使用指南
  • Windows下开箱即用的音视频转码工具包,含全格式编解码支持
  • Linux 下删库跑路的正确姿势?别怕,教你数据恢复全流程
  • 2026国内最有名起名老师推荐.起名大师推荐. - 资讯纵览
  • FitGirl游戏启动器完整指南:一站式管理压缩游戏的终极解决方案
  • SpringBoot+Vue 农商对接系统管理平台源码【适合毕设/课设/学习】Java+MySQL
  • 蚂蚁搬家难易程度划分
  • 告别臃肿安装!手把手教你为Zynq-7000定制最小化的Vivado 18.3开发环境
  • 3分钟免费激活Windows和Office:KMS_VL_ALL_AIO一键智能激活方案
  • GraphRAG 生产配置:多模型策略怎么选,成本怎么控
  • 2026白云区搬家公司终极评测排行|全域覆盖+价格透明+安全保障优质服务商全解析 - gzdjxd
  • 晶振采购实战指南:从参数到供应链,保障电子项目稳定心跳
  • 抖音视频无水印解析工具:3步获取纯净版短视频的终极方案
  • 石家庄起名馆排名.石家庄起名老师推荐.石家庄起名大师推荐 - 资讯纵览
  • 在Ubuntu 22.04上,5分钟搞定CloudCompare的Snap安装与基础点云查看
  • WzComparerR2技术解析:冒险岛WZ文件逆向工程的完整实现方案