新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java volatile与synchronized:原理、区别与选型实战

发布时间:2026/9/29 1:27:05来源:尧图网络
Java volatile与synchronized:原理、区别与选型实战
1. 从一段诡异的死循环说起我第一次被 volatile 和 synchronized 的区别拷打是在一个很普通的下午。代码逻辑简单得不能再简单一个布尔标志位running主线程把它改成 false工作线程在循环里检查它为 false 就退出。结果工作线程死活不退出日志一行都不打CPU 却跑满了。当时我以为是线程池的问题换成了Thread、换了 JDK 版本、甚至怀疑过机器最后才发现问题出在running这个变量没有加volatile。这个坑几乎每个写并发的人都会踩一次。它背后牵扯出的正是这篇文章要讲清楚的两个关键字volatile和synchronized。很多人对它们的印象是模糊的——都是保证线程安全的volatile 轻量synchronized 重——但真到了要选型的场景往往就卡壳了。这篇文章我会把这两个关键字的底层原理、适用边界、常见误用、性能实测、选型思路全部拆开讲不管你是刚接触并发的新手还是写过多线程代码但对内存模型没完全吃透的开发者看完应该都能对什么时候用哪个有个清晰的判断。需要先说明一点下面讲的 volatile 默认指 Java 的 volatile同时我也会专门拿 C 语言里的 volatile 做对比因为这两个同名关键字经常被人混淆而热词里也大量出现了volatile c语言、#define clkcon_uni ((volatile clkcon *) (sfr_base 0x00 * 4))这类嵌入式写法这块的差异讲清楚会省掉你很多困惑。2. 为什么一个关键字能看不见另一个线程的修改2.1 可见性问题的根源三层缓存与写缓冲要理解 volatile必须先理解一个问题为什么一个线程改了变量另一个线程会看不到。这不是 Java 独有的而是现代 CPU 架构带来的必然结果。现代计算机的内存层次大概是这样的CPU 寄存器最快但容量极小L1/L2/L3 缓存次之主内存最慢但容量最大。为了弥补 CPU 和主内存之间巨大的速度差CPU 会把频繁访问的数据缓存到自己的缓存里读的时候先查缓存写的时候也是先写缓存再在某个时机刷回主内存。单核时代这么做没问题因为只有一个核在看这些数据。但多核时代每个核心都有自己的缓存A 核改了数据只写进了 A 的缓存B 核读的还是自己缓存里的旧值看起来就是改动丢失了。除此之外还有写缓冲Store Buffer和无效队列Invalidate Queue的加入。即使缓存之间有一致性协议比如 MESI来同步状态为了性能写操作会先进写缓冲读操作会先进无效队列滞后处理。这就导致即便缓存协议在硬件层面最终一致程序看到的顺序和时机也未必符合直觉。所以说到底可见性问题的本质是一个线程的写和另一个线程的读发生在不同的缓存副本上中间还有缓冲机制延迟同步。volatile 要解决的就是这个看不到的问题。2.2 volatile 干了两件什么活Java 的 volatile 关键字本质上是让 JVM 帮你在变量读写前后插入内存屏障Memory Barrier也叫 Fence。它的语义可以拆成两条可见性对 volatile 变量的写会立刻刷到主内存准确说是让其他线程的缓存副本失效对 volatile 变量的读会强制从主内存或最新的缓存状态读取。这样 A 线程的写B 线程马上就能看到。禁止指令重排序编译器和 CPU 为了优化性能会重排指令顺序volatile 会限制屏障前后的指令不能随意跨过屏障重排。这里有个点特别容易被忽略volatile 不保证原子性。i这种看似简单的操作实际上是读 - 改 - 写三步volatile 只能保证读到的是最新值、写出去能被看到但两个线程同时读到同一个旧值再各自加一结果还是会丢更新。这是新手最容易踩的坑以为加了 volatile 就万事大吉。而 synchronized 的语义完全不同。它解决的是互斥问题同一时刻只允许一个线程进入临界区其他线程排队等待。它顺带也保证了可见性——因为线程在退出 synchronized 块时会刷新缓存进入时会重新读取所以在 synchronized 块内修改的变量下一个拿到锁的线程一定看得到。但我常跟人说一句话synchronized 的可见性是附带的互斥才是它的主业。你为了可见性去用 synchronized等于开着一辆卡车去买一瓶矿泉水。能用 volatile 的场景用 volatile 就够了。2.3 一个 bash 演示让问题具象化很多人看理论看半天没感觉跑一段代码立刻就有体感了。下面这段代码去掉 volatile 就会卡死加上就正常退出public class VisibilityDemo { // 去掉 volatile 试试大概率陷入死循环 private static volatile boolean running true; public static void main(String[] args) throws InterruptedException { Thread worker new Thread(() - { System.out.println(worker start); while (running) { // 故意空转让 JIT 有机会优化 } System.out.println(worker stop); }); worker.start(); Thread.sleep(1000); running false; System.out.println(main set runningfalse); worker.join(); } }去掉 volatile 之后worker 线程很可能一直不退出。原因在于 JIT 编译器发现while (running)这个循环体内没有对 running 的写操作就把 running 的值提升到了寄存器里做循环条件不再每次从主内存读。主线程改了主内存的值worker 却一直读自己寄存器里的旧值于是永远为真。这就是可见性丢失最经典的现场。注意这个现象依赖 JIT 优化有的机器可能碰巧不复现所以不要用它去做严谨验证。想稳定复现可以加-Xint关闭 JIT或调整循环复杂度来触发优化但真正的并发测试还是建议用 JCStress 这类专门工具。3. volatile 在 Java 和 C 里根本不是一回事3.1 Java volatile 保证的是并发语义Java 里的 volatile 是语言级别的并发原语JVM 和 JMMJava 内存模型明确规定了它的语义可见性 有序性禁止特定重排。JVM 会在底层翻译成对应平台的内存屏障指令比如 x86 上 volatile 写会编译成带lock前缀的指令这个前缀会强制刷写缓冲并同步缓存。我见过不少人的误区是volatile 就是给变量加个内存屏障把值刷来刷去。 这个理解不算错但太机械了。更准确的说法是volatile 建立了一种happens-before 关系——对 volatile 变量的写 happens-before 后续对它的读。这个关系是 JMM 给程序员的承诺也是并发正确性推理的基础。3.2 C 语言 volatile 管的是不要优化掉访问C 语言以及 C里的 volatile 完全是另一套东西。它不保证线程安全不保证原子性不提供内存屏障也不保证多线程可见性。它只告诉编译器一件事这个变量可能被程序之外的因素改变不要把它优化到寄存器里每次访问都老老实实去内存读写。那它用在哪典型场景是内存映射的硬件寄存器。你热词里那个#define clkcon_uni ((volatile clkcon *) (sfr_base 0x00 * 4))就是干这个的——嵌入式芯片里某个时钟控制寄存器映射到某个地址硬件会自己改它的值编译器如果把它优化进寄存器读到的就是过时的值。加 volatile 就是警告编译器别自作聪明。还有个经典场景是信号处理函数里修改的变量或者裸机环境下中断服务程序与主循环共享的变量。这些场景的共同特点是改动来源在编译器视野之外编译器必须每次都真去读内存。3.3 别把两者混为一谈这里给一个特别容易搞混的对照表我建议直接记下来对比维度Java volatileC/C volatile解决的核心问题多线程可见性、禁止重排序防止编译器优化掉访问是否保证原子性否只保证单个读/写原子不保证复合操作否是否提供内存屏障是JVM 插入屏障否标准不保证编译器一般不插是否适用多线程同步是配合其他手段完成同步否不能用来做线程同步典型场景状态标志位、单次发布引用硬件寄存器、信号处理、裸机中断我在面试里问过不下几十个人Java 的 volatile 和 C 的 volatile 有什么区别能答到一个管并发、一个管编译器优化这个层面的不到三成。如果你正准备面试并发相关岗位这条务必搞清楚因为它能直接区分背过八股和真理解。提示C11 之后引入了std::atomic如果你在 C 里要做多线程共享变量请用std::atomic而不是volatile。volatile 在 C 里同样只管优化不管并发。这个坑我实打实见过来自生产环境的事故排查。4. synchronized 的进化史与锁升级路径4.1 从重量级到自适应早年间 synchronized 的名声不太好被叫做重量级锁。因为它依赖操作系统的互斥量mutex加锁解锁都要陷入内核态线程阻塞、唤醒涉及用户态和内核态切换开销很大。所以那时候大家都推荐用ReentrantLock或者原子类来替代。但 JDK 6 之后HotSpot 对 synchronized 做了一系列优化引入了偏向锁Biased Locking、轻量级锁Lightweight Locking、自旋锁Adaptive Spinning形成了锁升级路径。简单说无锁 → 偏向锁如果只有一个线程反复获取同一把锁JVM 直接把锁标记偏向这个线程后续进入几乎零开销连 CAS 都不用做。偏向锁 → 轻量级锁出现第二个线程竞争时偏向锁撤销升级为轻量级锁用 CAS 自旋来竞争。轻量级锁 → 重量级锁自旋到一定次数还没拿到锁就升级为重量级锁真正阻塞线程。所以现在说synchronized 慢多数情况下是不成立的。没有竞争或者竞争不激烈时它的性能和 ReentrantLock 差不了多少甚至更优。而且它不需要手动释放异常自动释放锁用起来更安全。4.2 锁升级的状态记在哪锁状态信息记在对象头的 Mark Word 里。64 位 JVM 下一个对象头一般占 12 字节8 字节 Mark Word 4 字节类型指针开启压缩指针时Mark Word 会随着锁状态变化记录不同内容偏向锁记线程 ID轻量级锁记指向栈中锁记录的指针重量级锁记指向 Monitor 的指针。理解这个有助于你知道为什么 Java 里任何对象都能当锁——因为每个对象头里都预留了锁状态的位置。顺带说个实践里常被问的问题锁对象的选择。用this当锁锁的粒度是这个实例用ClassName.class当锁锁的是整个类的所有实例。很多单例写错、并发控制失效的事故都是因为锁对象选错了。我一般的建议是专门定义一个private final Object lock new Object();作为锁对象语义清晰且不会被外部意外拿到避免锁竞争范围失控。4.3 自旋锁不是越多越好自旋锁的原理是拿不到锁时不立即阻塞而是空转几圈看看锁会不会很快被释放。这适用于锁持有时间短的场景因为线程阻塞/唤醒的上下文切换开销往往比自旋几十上百个时钟周期还大。但自旋也有代价如果锁一直被持有自旋就是纯浪费 CPU。所以 HotSpot 用的是自适应自旋——根据上一次在同一把锁上的自旋成功率动态调整自旋次数。简单说就是上次自旋成功了这次多转几圈上次失败了这次少转或直接阻塞。我实测过一个高竞争场景把同步块内容从几行内存操作改成包含一次 IO 调用后吞吐量断崖式下跌。原因就是锁持有时间变长自旋全部失败大批线程进入重量级锁阻塞上下文切换爆炸。所以在 synchronized 里做耗时操作是性能大忌能拆就拆能减小锁粒度就减小。5. 选型实战什么场景用哪个5.1 决策清单我把日常用到的判断整理成一张表照着场景对号入座就行场景推荐方案理由单个状态标志位如 running、初始化标记volatile只需可见性无需互斥单次安全发布对象引用volatile配合 final 字段避免半初始化对象逸出计数器累加isynchronized / AtomicIntegervolatile 不保证原子性复合条件判断后修改synchronized需要互斥保护检查 - 修改整体一次修改多个相关变量需保持一致synchronizedvolatile 无法保证多变量原子性读多写少、且写操作是简单赋值volatile性能开销最小临界区含耗时操作尽量缩小同步块或改用并发容器减少锁持有时间5.2 一个真实场景的演进过程我之前维护过一个设备状态缓存需求是后台线程定期刷新设备状态前端查询时拿到最新的状态。最初实现是一个HashMap加 synchronized 方法读写都加锁。压测时 QPS 上不去锁竞争严重。第一次优化把读改成 volatile 引用发布——用不可变对象封装整个状态快照写的时候构建新快照再一次性赋给 volatile 引用读的时候直接读引用。这样读操作完全无锁只有写操作需要重建对象性能提升非常明显。核心思路是利用 volatile 保证引用发布的可见性同时用不可变对象避免读到中间状态。第二次优化写操作频率其实很低干脆用AtomicReference配合 CAS 替换 volatile 引用保证并发写时只有一个成功。这里之所以能用 CAS 而不是锁是因为状态替换本身是单变量原子操作不需要跨变量一致性。但如果需求变成同时更新设备数量和时间戳且两者必须一致那就只能回到 synchronized。这个演进过程把两个关键字的边界体现得淋漓尽致volatile 能解决可见性和发布问题但一旦涉及多个操作要作为一个整体或复合条件判断就必须请出 synchronized。5.3 我踩过的坑与避坑清单讲几个实操里真栽过的跟头比理论好使坑一给数组加 volatile。volatile int[] arr只保证数组引用本身的可见性不保证数组元素的可见性。要保证元素可见得用AtomicIntegerArray或者对元素操作加锁。坑二以为 volatile 加了就线程安全。前面说的 i、先检查后操作、多变量关联修改volatile 全都护不住。坑三synchronized 锁了不同对象。两个方法用 this 锁但如果通过不同实例调用锁的其实是不同对象照样并发。日志里看不出问题因为每个实例内部确实是串行的。坑四在同步块里调用外部方法。外部方法可能耗时、可能再来锁容易死锁或长时间持锁。原则是同步块里只放该放的东西。提示如果一段代码既需要可见性又需要原子性不要纠结用 volatile 还是 synchronized直接用 synchronized 或对应原子类简单可靠。过早优化引入的复杂度往往比那点性能收益贵得多。6. 常见问题与排查实录6.1 死循环不退出怎么定位我一般按这个顺序查先确认循环条件变量有没有 volatile再看循环体里有没有可能触发 JIT 把变量提升到寄存器的操作比如循环体为空、或只读不写然后用-Xint或-XX:-UseCompile跑一遍看现象是否消失消失基本就锁定是可见性问题。更稳妥的方式是用线程 dumpjstack看 worker 线程卡在哪个方法上如果卡在纯 CPU 循环里基本就是这个原因。6.2 synchronized 导致性能毛刺怎么排查典型的症状是平时吞吐正常偶发几百毫秒到几秒的停顿。排查手段是抓 thread dump看是不是大量线程 BLOCKED 在同一把锁上再用 JFR 或 async-profiler 看锁竞争火焰图找到热点锁最后看临界区代码通常能在里面揪出一两次不该有的 IO、日志、或者大对象创建。把临界区缩到最小后毛刺基本就消失了。6.3 volatile 到底管不管有序性管但要限定范围。volatile 保证的是对 volatile 变量本身的读写不会与其他内存操作任意重排volatile 写之前的操作不能排到写之后volatile 读之后的操作不能排到读之前。这叫半屏障效果。但两个普通变量之间即使都在 volatile 读写附近它们的相互顺序也不是 volatile 保证的。所以别把 volatile 当万能有序性武器涉及多变量顺序依赖时还是得靠锁或者明确的同步原语。6.4 常见误区速查误区表述实际情况volatile 保证原子性不保证只保证单次读写可见与有序volatile 能替代锁只在无复合操作时能替代其他不行synchronized 一定比 volatile 慢很多无竞争或轻竞争时差距很小C 的 volatile 能用于多线程同步不能只是防编译器优化volatile 数组元素也可见不只保证数组引用的可见性加了 synchronized 就万事大吉还得看锁对象、锁范围、是否死锁7. 一点个人的选型经验用了这么多年我总结出一条特别朴素的原则能用 volatile 解决的绝不上锁不能确定能不能用的先用锁。因为锁的语义更好推理出错概率低性能也未必差。追求无锁性能的优化应该发生在被压测证明是瓶颈之后而不是一开始就上。另外我强烈建议你在项目里把并发相关的代码单独立一个模块或者包写清楚每个共享变量的并发语义——是 volatile 可见性、还是锁保护、还是不可变。很多线上并发 bug 之所以难查不是因为技术难而是因为三个月后没人记得这个变量当初为什么这么写。注释里写一句本字段由 XXX 锁保护本字段 volatile 仅用于状态发布能救后来人一命也能救你自己。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

简历技术栈全面复习——Python全栈 2026/9/29 2:12:37

简历技术栈全面复习——Python全栈

Web浏览器│↓Flask-SocketIO│↓Python测试平台│┌────────────┼────────────┐↓ ↓ ↓CAN测试 IMU测试 EtherCAT测试│↓C控制程序│↓电机/设备同时 Python 还负责:测试任务↓ 启动程序↓ 监控程序↓…

阅读更多 →
干货分享 | TSMaster图形模块功能详解(二)—— 以CAN信号为例 2026/9/29 2:12:30

干货分享 | TSMaster图形模块功能详解(二)—— 以CAN信号为例

在上一章节中,我们主要分享了TSMaster图形模块功能中信号的导入与删除、图形分栏、暂停与启动和禁止图形、高亮信号相关操作、预设、信号与数据的导入与导出6大模块的操作教程。本章节在上一篇基础上,继续介绍TSMaster图形模块功能第7~10模块的教程。本文…

阅读更多 →
【GitHub项目实战】Translators 实现多语言翻译 2026/9/29 2:12:30

【GitHub项目实战】Translators 实现多语言翻译

随着全球化的深入发展,跨语言的交流需求愈发重要。在编程中,能够高效且准确地实现多语言翻译成为了一个广泛应用的场景。无论是自动化的文档翻译,用户界面的国际化,还是实时的跨语言交流,翻译解决方案在实际项目中都扮演了至关重要的角色。本文将探讨如何使用 Translators…

阅读更多 →
LangChain Component Architecture 深度解析:从模型、工具到 Agent 的组件化架构 2026/9/29 2:12:30

LangChain Component Architecture 深度解析:从模型、工具到 Agent 的组件化架构

学习 LangChain 时,很容易陷入一个个 API: ChatOpenAI()create_agent()toolRetriever()VectorStore()看起来每个组件都很重要,但如果只记 API,很难真正理解:LangChain 到底是如何把这些组件组合成一个 AI 应用的&#…

阅读更多 →
【GitHub项目实战】VideoRetalking 实现音频驱动的数字人口型同步 2026/9/29 2:12:30

【GitHub项目实战】VideoRetalking 实现音频驱动的数字人口型同步

深度合成技术正不断改变语音驱动视频生成的方式,VideoRetalking 项目通过将输入音频与静态人脸图像或动态视频进行高精度的唇形同步,实现了数字人口型自动对齐。这项技术结合多种预训练模型与图像重建算法,具备良好的生成质量与适应性,特别适用于构建更真实的人机交互场景。…

阅读更多 →
C++运算符重载全解析:规则、选型与工程实践 2026/9/29 2:12:30

C++运算符重载全解析:规则、选型与工程实践

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉