尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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 是「可见性 有序性」,不是「原子性」——它守的是标志位,不是计数器。
RELATED

相关推荐

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

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

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

📅 2026/9/1 2:21:30
Moneta Markets亿汇:新手更在意的客户支持,这里做个要点解读

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

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

📅 2026/9/10 18:31:28
测试文章 001644 - 请忽略

测试文章 001644 - 请忽略

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

📅 2026/9/10 2:14:39
MORE NEWS

更多资讯

📰

OpenClaw 在Centos7上快速部署与简单验证

OpenClaw 在Centos7上快速部署与简单验证 Install # 添加NodeSource仓库 curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash - # 安装Node.js sudo yum install -y nodejs # 验证安装 node --version npm --version# 安装OpenClaw curl -fsSL https://openclaw.ai…

📰

DeepSeek - 尝试一下以Responses API方式去使用DeepSeek模型

1. 简单介绍 当前AI大模型以及Agent技术迅速普及和推广,DeepSeek公司也在这波大潮流中得到很大的发展,下面是forbes的数据, 之前在查看DeepSeek技术资料的时候,deepseek-flash和deepseek-v4-pro不是都支持Response API的&#xf…

📰

ESP32脑电波控制家电:NPG Lite与红外模块实战

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

📰

Linux系统调试课(CPU篇)CPU亲和性与多进程调度

文章目录 一、概述 二、为什么需要 CPU 亲和性 2.1 跨核迁移的代价 2.2 亲和性分类 2.3 负载均衡与亲和性的关系 三、基本概念 3.1 cpumask 与亲和性掩码 3.2 中断亲和性原理 四、源码解析:__set_cpus_allowed_ptr 4.1 函数原型 4.2 SDK 真实实现 4.3 执行流程图 五、RK3506 设…

📰

Razzle 接入 LESS:razzle-plugin-less 插件安装、配置项与 loader 处理链路全解析

前端构建工具前端构建后端 【免费下载链接】razzle ✨ Create server-rendered universal JavaScript applications with no configuration 项目地址: https://gitcode.com/gh_mirrors/ra/razzle 点击查看 免费下载 本指南围绕 Razzle 官方插件 razzle-plugin-less…

📰

深度 | 云栖大会阿里亮底牌:模型芯片云三箭齐发,正式站上追ASI的跑道

2026 云栖大会,阿里把「AI 模型、AI 芯片、AI 云」三个词摆到战略 C 位。吴泳铭说未来机器思考总量达人类 1000 倍,平头哥亮出 3 倍算力的真武 V900,Qwen 公开了迈向 ASI 的路线图。 我判断,这届云栖的分量不在单个产品&#xff0…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬