当前位置: 首页 > 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.rkmt.cn/news/1482164.html

相关文章:

  • 台州云栖雅筑(宸智雅筑)装饰官方联系方式 合作电话 官网入口 避坑指南 - 资讯纵览
  • Node-RED Dashboard终极指南:3步打造专业级可视化界面
  • Node.js版本太低?手把手教你用NVM切换版本,解决NPM安装时的EUNSUPPORTEDPROTOCOL错误
  • openLCA 2.6.2 终极指南:免费开源的生命周期评估解决方案
  • Linux 下删库跑路的正确姿势?别怕,教你数据恢复全流程
  • 2026国内最有名起名老师推荐.起名大师推荐. - 资讯纵览
  • 石家庄起名馆排名.石家庄起名老师推荐.石家庄起名大师推荐 - 资讯纵览
  • 深度解析Realtek RTW89无线网卡驱动:Linux系统下WiFi 6/7设备完整技术指南
  • 从零构建3D打印切片软件:BambuStudio开源贡献实战指南
  • 从LED到单片机:硬件焊接与编程实践全解析
  • 2026番禺搬家公司终极评测指南|口碑性价比双维度实测排行+本地避坑全攻略 - gzdjxd
  • 从诗词到词元:青年见证传统文化与数字文明的时代交融
  • Windows安卓应用安装器:3分钟实现电脑运行安卓应用
  • 092、编队飞行:一致性理论
  • 如何5分钟搞定Mac Boot Camp驱动自动化部署:Brigadier终极方案
  • 2026年国内区域优质深山天然饮用水厂家精选榜单 - 企业推荐师
  • 一文搞懂:Java与Web3交互实战——用Java构建区块链应用后端
  • Redis突然变慢了?你可能踩了这几个隐蔽的坑
  • 中通快递10斤要多少钱?2026最新寄件省钱攻略 - 快递物流资讯
  • 终极指南:如何让老款Mac重获新生——OpenCore Legacy Patcher完整教程
  • 东莞墙面刷新多少钱一平方?2026年报价明细+品牌对比+怎么选 - 优家闲谈
  • 为什么你的微服务改造总失败?谈谈领域驱动设计的落地痛点
  • “照得标”下载页面
  • 天津品牌小程序制作怎么选 2026 精选榜单参考 | 6月最新整理 - 软件测评师
  • CSDN AI数字营销企业采购必读:团购门槛、账号绑定规则、续费锁价机制(内部渠道限时开放中)
  • Prometheus + Grafana 云原生可观测性体系:从指标采集到告警闭环的完整实践
  • 从零到一:Happy Island Designer 终极实战指南 [特殊字符]️
  • 2026年济南驾校大揭秘:哪家学员数量最多?速来一探究竟! - 资讯纵览
  • 拯救你的代码规范:手把手教你配置STS的代码模板与实时检查(告别脏乱差)
  • Kubernetes 生产环境排障实录:典型故障案例与诊断方法论