Java volatile 到底解决什么问题:可见性、禁止重排与双重检查锁单例
Java volatile 到底解决什么问题:可见性、禁止重排与双重检查锁单例面试问 volatile,十有八九会答「保证可见性、不保证原子性」。这话没错,但真到写代码时就懵了:什么时候该加 volatile?为什么单例的双重检查锁一定要加它?加了 volatile 的count为什么还是会错?这篇把这三件事一次讲透,全是能跑的代码。先看一个「死循环」bug下面这段代码,主线程想通过改running来让子线程停下:publicclassStopThread{// 没加 volatileprivatestaticbooleanrunningtrue;publicstaticvoidmain(String[]args)throwsInterruptedException{newThread(()-{longi0;while(running){// 子线程一直读 runningi;}System.out.println(stopped, ii);}).start();Thread.sleep(1000);runningfalse;// 主线程改成 falseSystem.out.println(main set runningfalse);}}直觉上子线程一秒后就该停。但在 server 模式 JVM(-server,生产默认)下,这个程序大概率永远不停。原因不是「主线程没改成功」,而是子线程根本没看见改动。JIT 编译器看到running在循环里从没被当前线程修改,就把它优化成了:if(running){while(true){i;}// running 被提到循环外,只读一次}子线程读的是自己工作内存里的副本,主线程的写停留在它自己那边,两边没同步。这就是可见性问题。volatile 修复可见性给running加上 volatile:privatestaticvolatilebooleanrunningtrue;再跑,子线程立刻就停了。volatile 干了两件事保证可见性:写一个 volatile 变量时,JVM 会把当前线程工作内存里的值立刻刷回主内存。读一个 volatile 变量时,JVM 会让当前线程工作内存里的副本失效,强制从主内存重新读。所以主线程runningfalse一写,子线程下一次while(running)就能读到最新值。JIT 也不敢再把它缓存到寄存器里。一句话:volatile 保证一个线程的写,对其他线程立即可见。这是它最主要、也最该用的场景——「状态标志位」。但 volatile 不保证原子性,别拿它做计数器很多人以为 volatile 变量的就线程安全了,这是最常见的误用:publicclassCounter{privatestaticvolatileintcount0;publicstaticvoidmain(String[]args)throwsInterruptedException{Runnabletask()-{for(inti0;i10000;i){count;// 期望最终是 100000}};Thread[]tsnewThread[10];for(inti0;i10;i){ts[i]newThread(task);ts[i].start();}for(Threadt:ts)t.join();System.out.println(count);// 实际几乎每次都 100000}}跑出来是 8 万多、9 万多,反正不是 10 万。因为count不是一条指令,它是三步:读 count 到寄存器寄存器 1写回 countvolatile 只保证每一步读到/写出的是最新值,但保证不了这三步中间不被别的线程插队。两个线程同时读到 100,各自 1,各自写回 101,一次自增就丢了。计数器要用AtomicInteger(底层 CAS)或synchronized:privatestaticfinalAtomicIntegercountnewAtomicInteger(0);// ...count.incrementAndGet();// 这才是原子自增记住判断标准:volatile 只适合「一个线程写、多个线程读」的标志位;只要有「读-改-写」这种复合操作,就得上原子类或锁。volatile 的第二个作用:禁止指令重排这是双重检查锁单例(DCL)必须加 volatile 的原因,也是最容易被跳过的知识点。先看没加 volatile 的经典 DCL:publicclassSingleton{privatestaticSingletoninstance;// 没加 volatile,有坑publicstaticSingletongetInstance(){if(instancenull){// 第一次检查(不加锁,快)synchronized(Singleton.class){if(instancenull){// 第二次检查(加锁,防重复创建)instancenewSingleton();// 问题就出在这一行}}}returninstance;}}instance new Singleton()看着是一行,实际是三步:分配内存调用构造函数初始化对象把instance指向这块内存问题在于:JVM 允许在不影响单线程结果的前提下重排指令,把顺序变成 1 → 3 → 2。即「先让 instance 指向内存,再初始化」。于是可能出现这个时序:线程 A 执行到「1 → 3」,instance已经非 null,但对象还没初始化完。线程 B 走第一次检查instance null,发现不为 null,直接return instance。线程 B 拿到一个半成品对象,访问它的字段可能是默认值甚至崩溃。给instance加上 volatile 就能禁止这种重排:publicclassSingleton{privatestaticvolatileSingletoninstance;// 加上 volatilepublicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();// volatile 保证 2 一定在 3 之前}}}returninstance;}}volatile 会插入内存屏障,禁止步骤 2、3 之间重排,保证「对象完全初始化后,instance 才指向它」。这就是 DCL 里 volatile 不可省的原因。顺带一提:如果你不需要懒加载的极致控制,静态内部类方式写单例更简单,天然线程安全且懒加载,连 volatile 都不用操心:publicclassSingleton{privateSingleton(){}privatestaticclassHolder{// 类加载机制保证 INSTANCE 只初始化一次,且线程安全staticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;// 第一次调用才触发 Holder 类加载}}happens-before:volatile 还能「捎带」普通变量volatile 有个常被忽视的能力:volatile 写之前的所有普通变量写,对 volatile 读之后的操作都可见。这叫 happens-before 规则。publicclassConfig{privateintport;// 普通变量privateStringhost;// 普通变量privatevolatilebooleanreadyfalse;// volatile 标志位// 线程 A:先填数据,最后置 readypublicvoidinit(){port8080;host10.0.0.1;readytrue;// volatile 写,前面两个普通写都会刷到主内存}// 线程 B:看到 ready 就能安全读 port/hostpublicvoiduse(){if(ready){// volatile 读connect(host,port);// 保证读到 8080 和 10.0.0.1,不会是默认值}}}只要ready用 volatile,线程 B 一旦读到ready true,就一定能看到线程 A 在此之前写的port、host,哪怕它们本身不是 volatile。这是用一个 volatile 标志位「守护」一组数据的常见手法。小结volatile 解决可见性:一个线程的写立即对其他线程可见,最适合「状态标志位」(如 running、shutdown、ready)。volatile 不解决原子性:count这种读-改-写复合操作照样出错,计数器用AtomicInteger或synchronized。volatile 禁止指令重排:双重检查锁单例里的instance必须加 volatile,否则可能拿到半初始化对象;不追求懒加载精细控制就用静态内部类单例。happens-before:volatile 写能「捎带」它之前的普通变量写,让读到标志位的线程安全看到那组数据。记忆点:volatile 是「可见性 有序性」,不是「原子性」——它守的是标志位,不是计数器。

相关新闻

Agent编排:别再纠结选LangChain还是LangGraph了

Agent编排:别再纠结选LangChain还是LangGraph了

内容速览章节核心内容一、编排到底是什么Workflow vs Agent、编排在架构中的位置二、七种编排模式Prompt Chaining / Routing / Parallelization / ReAct / Plan-and-Execute / Orchestrator-workers / Evaluator-Optimizer三、生产环境绕不开的问题会话管理、并发控制、错误分…

2026/7/23 0:18:29 阅读更多 →
Moneta Markets亿汇:新手更在意的客户支持,这里做个要点解读

Moneta Markets亿汇:新手更在意的客户支持,这里做个要点解读

在外汇行业语境里,表达越清晰、信息越透明,越容易建立稳定预期。在Moneta Markets亿汇的外汇服务中,从公开信息与使用体验出发,梳理其更值得肯定的能力点与细节表现。外汇相关信息更新频繁,平台将关键提示与解释呈现得…

2026/7/23 0:18:29 阅读更多 →
垂直领域的那些事儿——遥感、医疗、工业质检,各有各的苦

垂直领域的那些事儿——遥感、医疗、工业质检,各有各的苦

最后一篇了,咱们不聊通用分割了,聊聊垂直领域。这些领域里的语义分割,玩的逻辑跟学术数据集完全是两码事。先说 遥感图像分割。这玩意儿的分辨率动不动就是 0.5 米到 2 米每像素,一张图覆盖几平方公里,尺寸可达 10000x…

2026/7/23 0:17:29 阅读更多 →

最新新闻

非接触式便携微型生理变异性监测设备的技术现状

非接触式便携微型生理变异性监测设备的技术现状

1. 执行摘要 本报告旨在系统性地回答一个核心问题:在当前的消费级与医疗级市场中,是否存在已通过权威认证、能够准确可靠地检测心率、呼吸频率及其变异性(HRV/RRV)的非接触式、便携式、微型化生理监测设备。若不存在,本…

2026/7/23 0:54:40 阅读更多 →
YOLOX 自定义目标检测训练实战:VOC 数据、训练评估与预测展示

YOLOX 自定义目标检测训练实战:VOC 数据、训练评估与预测展示

YOLOX 自定义目标检测训练实战:VOC 数据、训练评估与预测展示 这篇教程根据我复现 YOLOX 目标检测流程时整理,重点演示依赖安装、VOC 数据接入、类别配置、训练评估和单图预测可视化。 本文整理自我的学习和项目复现过程,尽量按实操顺序保留…

2026/7/23 0:53:40 阅读更多 →
Java 继承②

Java 继承②

目录1. super 关键字2. super 和 this3. this和super访问时的内存布局1. super 关键字 在父类成员变量和子类成员变量名字相同的情况下,为了在子类中访问父类的成员。 super关键字的用法 : super.父类成员变量 class Person{String name;int age 10;public void …

2026/7/23 0:53:40 阅读更多 →
Kafka与Zookeeper集群部署实战指南

Kafka与Zookeeper集群部署实战指南

1. Kafka与Zookeeper集群部署核心解析Kafka作为分布式消息系统的标杆,其高吞吐、低延迟的特性使其成为现代大数据架构的核心组件。而Zookeeper作为Kafka的"中枢神经系统",负责维护集群元数据、选举控制器节点以及监控Broker状态。这套组合在金…

2026/7/23 0:53:40 阅读更多 →
LangChain Memory 记忆系统:让 AI 记住对话的魔法

LangChain Memory 记忆系统:让 AI 记住对话的魔法

引言:为什么 AI 需要“记忆”? 在与 ChatGPT 等大模型对话时,你是否曾感到一丝“挫败”?你刚刚在上一轮对话中详细说明了需求,下一轮它却仿佛失忆般问道:“您能再详细描述一下吗?” 这种“金鱼式…

2026/7/23 0:52:40 阅读更多 →
含多类型V2G电动汽车的微网日前-日内两阶段协同优化调度模型研究(Matlab代码实现)

含多类型V2G电动汽车的微网日前-日内两阶段协同优化调度模型研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 &#x1f381…

2026/7/23 0:52:40 阅读更多 →

日新闻

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击: https://intelliparadigm.com 第一章:从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表) 当AI副业主理人不再仅满足于单次服务交付,而是主动构建可复用、可裂变、可…

2026/7/23 0:00:25 阅读更多 →
AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

更多请点击: https://codechina.net 第一章:AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析 在对2,346篇跨行业AI生成文案的A/B测试数据进行聚类分析后,我们发现&#xff1…

2026/7/23 0:01:26 阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:01:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 8:58:19 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 19:43:43 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 12:54:44 阅读更多 →

月新闻