尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

volatile关键词:Java 可见性问题详解与示例:为什么线程写了值,另一个线程却看不见?

volatile关键词:Java 可见性问题详解与示例:为什么线程写了值,另一个线程却看不见?
📅 发布时间:2026/6/21 4:10:41

volatile关键词:Java 可见性问题详解与示例:为什么线程写了值,另一个线程却看不见?

Java 可见性问题详解与示例:为什么线程写了值,另一个线程却看不见?

在多线程编程中,你可能遇到这样一个匪夷所思的现象:
一个线程明明修改了变量,另一个线程却迟迟“看不到”变化,一直在死循环。这并不是 Java 的 bug,而是 Java 内存模型(JMM) 中“可见性”问题的直接体现。

本文用一个真实代码示例带你彻底搞懂:

为什么线程写了值,另一个线程仍然读不到?
什么是“可见性”?volatile 到底做了什么?
为什么写线程都结束了,读线程还卡住?


一、现象重现:写了值,却没被看到

我们用下面这段简单的代码来演示可见性问题:

package com.ne.scrm.kanban;public class VisibilityDemo {private static boolean flag = false; // 没有volatilepublic static void main(String[] args) throws Exception {Thread reader = new Thread(() -> {System.out.println("Reader started...");while (!flag) { } // 一直等待flag变为trueSystem.out.println("Reader detected change!");});Thread writer = new Thread(() -> {try { Thread.sleep(1000); } catch (InterruptedException ignored) {}flag = true; // 修改flagSystem.out.println("Writer set flag=true");});reader.start();writer.start();}
}

你可能以为这段程序会在 1 秒后输出:

Writer set flag=true
Reader detected change!

但实际情况是——

程序经常会卡死,读线程永远跳不出循环!


二、为什么读线程看不到最新值?

在 Java 内存模型(JMM)中:

  • 每个线程都有自己的工作内存(线程本地副本);

  • 主内存存放着所有共享变量的“真实值”;

  • 线程对变量的读写其实是对副本进行操作;

  • 是否、何时把工作内存的变化同步到主内存,由 JMM 规则决定。

(示意图:写线程更新了主内存,但读线程仍在读自己的旧副本)

在上面的示例中:

  1. 写线程 将 flag 改为 true,但这个修改只发生在自己的工作内存;

  2. 它没有强制刷新主内存;

  3. 读线程 一直从自己缓存的副本里读取旧值 false;

  4. 因此它永远认为 flag 没变,陷入死循环。


三、为什么“写线程结束了”也没用?

很多人会问:

写线程都结束了,按理 JVM 应该刷新数据吧?
为什么读线程还是看不到?

因为——

  • 线程结束(Thread.run() 结束)本身不建立 happens-before 关系;

  • 没有任何同步动作(锁、volatile、join、并发工具);

  • 也就没有可见性保证;

  • 读线程不会被强制重新读主内存,可能永远用旧值。

换句话说:写线程写进主内存 ≠ 读线程会再去主内存取。

JIT 优化甚至可能把 while(!flag) 优化成:

boolean cached = flag;
while (!cached) { }  // 永远不再重新加载

这就是为什么在某些机器上、某些 JVM 参数下,你会看到它“永远卡死”。


四、如何修复:使用 volatile

最简单有效的办法是在 flag 前加上 volatile:

private static volatile boolean flag = false;

加上后再运行,输出变为:

Reader started...
Writer set flag=true
Reader detected change!

原理:volatile 的双重保证

  1. 可见性:写线程对 volatile 变量的写操作会立刻刷新到主内存;
  • 并使其他 CPU 缓存中对应的缓存行失效;
  1. 禁止重排:编译器/CPU 不会把与 volatile 操作相关的指令重排到它前后;
  • 写之后的操作不会被提前;

  • 读之前的操作不会被延后。

这在 JMM 层面上形成了一个规则:

对同一个变量的 “volatile 写 → volatile 读”
建立 happens-before 关系。


五、volatile 背后的底层动作

JVM 在编译 volatile 读/写指令时,会插入内存屏障:

操作类型 插入屏障 作用
volatile 写 StoreStore + StoreLoad 写入主内存,刷新缓存
volatile 读 LoadLoad + LoadStore 强制从主内存读取最新值

在硬件层面,x86 使用 lock 前缀指令或内存栅栏指令来保证:

  • 该变量所在缓存行被刷新到主内存;

  • 其他核心对应的缓存行标记为“无效”。


六、其他可见性解决方案

除了 volatile,还有几种方式能保证可见性:

方案 建立 happens-before 的方式
synchronized / Lock unlock → lock
AtomicBoolean 等原子类 内部用 volatile 实现
CountDownLatch、Future.get() 工具类内部用锁或 volatile
Thread.join() writer 的操作 → join() 返回

例如用 CountDownLatch:

CountDownLatch latch = new CountDownLatch(1);
Thread reader = new Thread(() -> {try { latch.await(); } catch (InterruptedException ignored) {}System.out.println("Reader detected change!");
});
Thread writer = new Thread(() -> {try { Thread.sleep(1000); } catch (InterruptedException ignored) {}latch.countDown();System.out.println("Writer released latch");
});

countDown() → await() 建立 happens-before,可见性自然得到保证。


七、总结:一句话记住可见性

可见性 = 写线程的修改能被读线程及时看到。

在 Java 中,只有在发生同步动作(volatile、锁、join、并发原语)时,JMM 才会保证线程之间的变量视图同步。否则,即使写入了主内存,读线程也可能永远停留在自己的旧副本上。


🔑 总结表:不同机制的可见性

机制 可见性 原子性 有序性 场景
普通变量 ❌ ❌ ❌ 不保证任何并发语义
volatile ✔ ✖ 部分✔ 状态标志、配置
synchronized/Lock ✔ ✔ ✔ 临界区
原子类 ✔ ✔ ✔ 无锁计数等
并发工具(Latch/Future等) ✔ ✔ ✔ 线程间通信

八、附:完整正确示例代码

package com.ne.scrm.kanban;public class VisibilityDemo {private static volatile boolean flag = false;public static void main(String[] args) {Thread reader = new Thread(() -> {System.out.println("Reader started...");while (!flag) {Thread.onSpinWait(); // JDK9+ 建议用于自旋等待}System.out.println("Reader detected change!");});Thread writer = new Thread(() -> {try { Thread.sleep(1000); } catch (InterruptedException ignored) {}flag = true;System.out.println("Writer set flag=true");});reader.start();writer.start();}
}

📘 结语

这就是典型的 Java 可见性问题。
写线程写到了主内存,但读线程从未去主内存重新读。
只有通过 volatile、锁或其他同步手段,才能让它们“对齐视图”。

相关新闻

  • UDP通信:解决socket连接关闭后缓冲内容未清除的问题
  • 在资源有限的M0单片机上运行RTOS
  • 2025年口碑好的国内螺杆真空泵厂家推荐及选择指南

最新新闻

  • 嵌入式GUI开发利器:emWin仿真工具从入门到精通实战指南
  • 谱截断归一化MMD:高效分布比较的核方法优化
  • 范畴论视角下的拓扑赋值转移:统一建模计算机科学中的结构与变换
  • LPC213x ARM7 Flash编程与调试实战:ISP/IAP命令详解与JTAG/ETM应用
  • 2026年评价高的山东镀锌链条/刮板机链条优质公司推荐 - 品牌宣传支持者
  • CSP实战指南:从HTTP头配置到React/Vite安全加固

日新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号