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:34阅读更多 →
Moneta Markets亿汇:新手更在意的客户支持,这里做个要点解读

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

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

2026/7/23 0:18:34阅读更多 →
测试文章 001644 - 请忽略

测试文章 001644 - 请忽略

这是一篇测试文章,用于验证账号状态,将立即删除。

2026/7/23 0:18:31阅读更多 →
深入解析I2C寄存器:从数据收发、时钟配置到中断管理的嵌入式驱动开发

深入解析I2C寄存器:从数据收发、时钟配置到中断管理的嵌入式驱动开发

1. I2C接口寄存器全景解析:从数据收发到中断管理的核心逻辑搞嵌入式开发,I2C总线绝对是绕不开的坎。它只有两根线,SDA数据线和SCL时钟线,结构简单,但真要把通信调稳定、调高效,里面的门道可不少。很多人调I…

2026/7/23 1:34:46阅读更多 →
嵌入式异构通信:MPC860与TMS320C6000 HPI接口硬件设计与时序验证

嵌入式异构通信:MPC860与TMS320C6000 HPI接口硬件设计与时序验证

1. 项目概述与核心价值在嵌入式系统,尤其是通信、音视频处理或工业控制这类对实时性和数据吞吐量要求极高的领域,单一处理器往往难以胜任。这时,异构多处理器架构就成了主流选择:用一个通用性强、控制逻辑复杂的微处理器&#xff…

2026/7/23 1:34:46阅读更多 →
DDIM技术解析:扩散模型加速与AIGC应用实践

DDIM技术解析:扩散模型加速与AIGC应用实践

1. 扩散模型加速革命:DDIM的技术突破与产业影响2015年首次提出的扩散模型,在2022年随着Stable Diffusion的发布彻底改变了AIGC领域的技术格局。但传统扩散模型面临的最大痛点始终是采样速度——生成一张高质量图像往往需要1000步以上的迭代计算。直到DDI…

2026/7/23 1:34:46阅读更多 →
GLM与Kimi编程工具对比:代码生成、调试与部署实战指南

GLM与Kimi编程工具对比:代码生成、调试与部署实战指南

1. 先搞清楚 GLM 和 Kimi 到底是什么关系如果你最近在关注 AI 编程工具,大概率会看到“GLM 大佬为 Kimi 打 Call”这类消息。简单说,GLM(通用语言模型)是智谱 AI 开源的系列大模型,而 Kimi 是月之暗面推出的智能助手&a…

2026/7/23 1:34:46阅读更多 →
Redis沙盒逃逸(CVE-2022-0543)

Redis沙盒逃逸(CVE-2022-0543)

一.沙箱的含义 沙箱是一种安全机制,限制程序只能访问特定资源,防止恶意操作影响系统。 场景 沙箱作用 浏览器沙箱 网页代码只能操作浏览器,无法直接读写本地文件 游戏沙箱 外挂程序无法修改游戏内存 Redis Lua 沙箱 Lua 脚本无法执行…

2026/7/23 1:34:46阅读更多 →
从聊天框到 Agent:Claude Code 真正的提效方式

从聊天框到 Agent:Claude Code 真正的提效方式

如此反复了四十分钟,最后我关掉 Claude Code,自己动手写,二十分钟搞定了。 这一刻我才真正理解 Claude Code 创始人 Boris Cherny 在斯坦福 CS146S 课上说的那句话——很多人把 Claude Code 用成了「纯聊天框」,看似提效&#xff…

2026/7/23 1:32:45阅读更多 →
Go语言静态资源打包方案对比与实践指南

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

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

2026/7/23 0:56:31阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

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

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

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

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

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

2026/7/23 0:56:31阅读更多 →
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:00:28阅读更多 →
从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

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

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

2026/7/23 0:00:28阅读更多 →
油泥处理设备哪里能买到

油泥处理设备哪里能买到

油泥处理设备哪里有?这是许多从事油田、炼化、清罐业务的从业者最关心的问题。根据河南三丰环保设备有限公司的行业经验,选购油泥处理设备的核心在于设备能否适配当地环保法规与原料特性,而非单纯看价格。该公司总经理王钦田先生指出&#xf…

2026/7/23 0:00:28阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/22 22:56:18阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/22 18:55:50阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/22 18:55:50阅读更多 →