`volatile` 到底保证了什么:可见性、有序性,和它管不到的原子性
前言有个 bug 特别典型一个线程把开关flag改成了true另一个线程里while (!flag)的循环却像没听见一样一直转个不停就是不退出。把flag加上volatile问题立刻消失。于是很多人得出一个模糊的结论“volatile就是个轻量级的synchronized加上就线程安全了。”这个理解害人不浅。因为紧接着他们会写出volatile int count; count;这种代码然后在多线程下发现count的最终结果总是对不上——明明加了volatile怎么还是不安全真相是volatile只保证两件事——可见性和有序性它从不保证原子性。上面第一个例子它能治第二个例子它治不了。这篇文章讲清楚volatile到底保证了什么、不保证什么底层靠什么实现以及什么场景该用、什么场景千万别用。环境说明本文基于 JDK 8HotSpot 虚拟机x86 架构。一、先复现复现 1可见性问题——不加volatile循环停不下来publicclassVisibilityDemo{// 注意这里没有 volatileprivatestaticbooleanflagfalse;publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadworkernewThread(()-{System.out.println(worker: 开始循环);while(!flag){// 空转等待 flag 变成 true}System.out.println(worker: 检测到 flagtrue退出);});worker.start();Thread.sleep(1000);// 让 worker 先转起来flagtrue;// 主线程改标志位System.out.println(main: 已把 flag 设为 true);}}按直觉主线程 1 秒后把flag改成trueworker应该马上退出。但实际运行-server模式或生产环境下worker: 开始循环 main: 已把 flag 设为 true worker 永远不打印退出程序挂住主线程明明改了flagworker却始终看到旧值false。只要给flag加上volatileprivatestaticvolatilebooleanflagfalse;worker立刻就能退出。这说明**“一个线程的修改另一个线程不一定看得见”**这个问题真实存在而volatile能解决它。复现 2原子性缺失——加了volatilecount照样出错那volatile是不是万能的再看一个publicclassAtomicityDemo{privatestaticvolatileintcount0;// 这次加了 volatilepublicstaticvoidmain(String[]args)throwsInterruptedException{Runnabletask()-{for(inti0;i10000;i){count;// 自增 1 万次}};Threadt1newThread(task);Threadt2newThread(task);t1.start();t2.start();t1.join();t2.join();System.out.println(期望20000实际count);}}两个线程各自增 1 万次期望结果是20000。但实际跑出来期望20000实际13472每次运行结果还不一样但几乎永远小于 20000。加了volatile也没用。两个现象放一起问题就来了同样是volatile为什么它能解决第一个死循环却解决不了第二个计数丢失二、根因/底层JMM 与三大特性要讲清楚这两个现象得先认识 Java 内存模型JMM。2.1 为什么会有可见性问题现代 CPU 有多级缓存。为了性能线程操作变量时并不是直接读写主内存RAM而是先把变量拷贝一份到自己的工作内存对应 CPU 寄存器、L1/L2 缓存里之后的读写都在工作内存进行什么时候刷回主内存不确定。JMM 把这个结构抽象成主内存所有线程共享 每个线程私有的工作内存。于是复现 1 的死循环就说得通了主线程把flag true写进了自己的工作内存还没来得及或没必要刷回主内存更关键的是worker线程在while (!flag)里反复读的是它自己工作内存里的副本那份副本一直是falseJIT 编译器甚至会把while (!flag)优化成if (!flag) { while (true) {} }——因为它认为循环体里没人改flag。结果就是主线程的修改worker永远看不见。这就是可见性问题。2.2volatile的可见性volatile对可见性的保证靠两条规则写一个volatile变量时JMM 会立即把工作内存的值刷回主内存读一个volatile变量时JMM 会强制从主内存重新读取让工作内存里的旧副本作废。所以flag加上volatile后主线程一改值马上进主内存worker每次循环都从主内存读自然就读到了true。可见性问题解决。2.3 有序性与指令重排volatile保证的第二件事是有序性。为了优化性能编译器和 CPU 会在不影响单线程结果的前提下对指令重新排序instruction reordering。单线程里这没问题但多线程下重排可能让另一个线程观察到诡异的执行顺序。volatile通过在变量读写前后插入内存屏障memory barrier来禁止这种重排屏障就像一道栏杆指令不允许越过它调换顺序。这保证了对 volatile 变量的写一定发生在后续对它的读之前也就是 JMM 里的happens-before规则。这一点在单例模式的双重检查锁定DCL里至关重要——不加volatile会读到半初始化的对象。这个话题留到下一篇专门讲。2.4 为什么原子性管不到现在回到复现 2为什么volatile int count的count还是错的关键在于count不是一个原子操作它实际是三步read从主内存读取count的当前值modify把值加 1write把结果写回主内存。volatile只保证每一步的读、写是可见的但它拦不住这三步在多线程下被切开、交错执行。看这个时序线程 A 读到count 100read时间片切换线程 B 也读到count 100read线程 A 算出 101写回write线程 B 也算出 101写回write。两个线程各自增了一次本该变成 102结果只有 101——一次自增被丢了。这就是为什么最终结果总是小于 20000。volatile的可见性在这里甚至帮了倒忙的忙它保证 B 每次读的都是最新值但当 A、B 几乎同时读时那个最新值对两人来说都是同一个旧值该丢还是丢。可见性 ≠ 原子性。三、正解volatile该用在哪、不该用在哪理解了三大特性用法就清晰了volatile适合一个线程写、多个线程读的状态标志不适合任何读-改-写的复合操作。适用场景状态标志位privatevolatilebooleanshutdownfalse;publicvoidshutdown(){shutdowntrue;}// 一处写publicvoidrun(){while(!shutdown){// 多处读doWork();}}写操作只是简单赋值不依赖旧值这正是volatile的主场轻量、无锁、保证可见。不适用场景读-改-写count、i i * 2、if (x null) x new X()这类依赖旧值的操作volatile都保证不了。该用// 方案 1加锁保证复合操作原子privateintcount0;publicsynchronizedvoidinc(){count;}// 方案 2CAS 原子类推荐无锁性能更好privateAtomicIntegercountnewAtomicInteger(0);publicvoidinc(){count.incrementAndGet();}// 方案 3高并发计数LongAdder 比 AtomicLong 更快privateLongAddercountnewLongAdder();publicvoidinc(){count.increment();}一个简单的决策原则只需要改了能被看见→volatile需要读-改-写整体不可分割→synchronized/AtomicXxxCAS/LongAdder。四、常见误区与面试高频问答Qvolatile是不是轻量级的synchronized不是。两者根本不是一个维度volatile只保证可见性 有序性不保证原子性也不提供互斥synchronized既保证可见性、有序性又保证原子性和互斥同一时刻只有一个线程进临界区。volatile是无锁的、更轻但能力也更弱。Qvolatile int i; i线程安全吗不安全。i是 read-modify-write 三步复合操作volatile只保证每步的读写可见拦不住三步被交错执行所以照样丢更新。要安全得用AtomicInteger或加锁。Qvolatile能保证a 1; b 2;这两行的原子性吗不能。volatile只作用于单个变量的单次读或写无法保证多个变量、多条语句的组合原子性。跨变量的原子操作只能靠锁。Qhappens-before 和内存屏障是什么关系happens-before 是 JMM 层面的规则/承诺volatile 写 happens-before 后续的 volatile 读内存屏障是 CPU/编译器层面实现这个承诺的手段。屏障禁止重排、强制刷缓存从而让 happens-before 成立。前者是是什么后者是怎么做到。Q64 位的long/double不加volatile会有什么问题JMM 规范允许 JVM 把 64 位变量的读写拆成两次 32 位操作。极端情况下一个线程可能读到写了一半的值高 32 位是新的、低 32 位是旧的即写撕裂。加volatile后JMM 保证long/double的读写是原子的。虽然主流 64 位 JVM 实际上已经原子处理但规范上不加volatile是有隐患的。总结volatile保证线程安全是个危险的误解。它的能力边界很清楚可见性✅写立即刷主内存、读强制读主内存一个线程的修改对其他线程立即可见——解决了复现 1 的死循环。有序性✅通过内存屏障禁止指令重排保证 happens-before——这是 DCL 单例必须加volatile的原因。原子性❌只保证单次读/写可见保证不了count这种读-改-写复合操作——这是复现 2 计数丢失的根因。一句话记忆volatile 可见性 有序性不含原子性。标志位一写多读放心用count读-改-写千万别用那是AtomicInteger和锁的活。

相关新闻

华为Bootloader解锁终极指南:用PotatoNV实现免拆机自由定制

华为Bootloader解锁终极指南:用PotatoNV实现免拆机自由定制

华为Bootloader解锁终极指南:用PotatoNV实现免拆机自由定制 【免费下载链接】PotatoNV Unlock the bootloader on Huawei devices with Kirin 620/65x/95x/960 项目地址: https://gitcode.com/gh_mirrors/po/PotatoNV 你是否对华为手机的系统限制感到束手无策…

2026/10/27 14:19:09 阅读更多 →
Unity 物理实战:射线检测实现角色瞄准与射击功能

Unity 物理实战:射线检测实现角色瞄准与射击功能

Unity 物理实战:射线检测实现角色瞄准与射击功能📚 本章学习目标:深入理解射线检测实现角色瞄准与射击功能的核心概念与实践方法,掌握关键技术要点,了解实际应用场景与最佳实践。本文属于《Unity工程师成长之路教程》U…

2026/10/28 2:18:40 阅读更多 →
从0到1搭建WAF检测环境:identYwaf安装与配置完全手册

从0到1搭建WAF检测环境:identYwaf安装与配置完全手册

从0到1搭建WAF检测环境:identYwaf安装与配置完全手册 【免费下载链接】identYwaf Blind WAF identification tool 项目地址: https://gitcode.com/gh_mirrors/id/identYwaf identYwaf是一款基于盲推理技术的Web应用防火墙(WAF)识别工具…

2026/10/28 9:52:29 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/27 17:33:32 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/28 7:28:00 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/28 9:02:11 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/27 18:20:50 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/28 9:00:54 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/28 7:29:02 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 16:58:40 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 9:01:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 19:25:09 阅读更多 →