Java并发可见性问题本质是线程修改共享变量后其他线程无法及时感知,根源在于JMM规定线程操作基于工作内存副本,而CPU缓存与缺乏同步机制导致主内存未及时更新或读取。

Java 中的并发可见性问题,本质是线程间对共享变量的修改无法及时被对方感知——不是代码写错了,而是 JVM 内存模型和 CPU 缓存机制在“悄悄作祟”。它不能靠加日志或打断点来稳定复现,因为调试行为本身会强制同步缓存、插入内存屏障,把问题“修复”了。
用最简代码复现可见性失效
下面这段代码能在多数 JDK(8u202+ / 11+ / 17+)上稳定复现:一个线程改标志位,另一个线程死循环读,却迟迟不退出:
- 定义一个普通 static boolean flag = true(不加 volatile)
- 启动子线程执行 while (flag) {} —— 它很可能永远卡住
- 主线程 sleep(100) 后设 flag = false
- 观察子线程是否退出:大概率不退出(尤其在无 I/O、无 sleep 的纯计算循环中)
关键点:去掉 System.out.println() 或 Thread.sleep(1)。这些看似无关的操作会触发内存屏障,让问题“消失”,造成“我本地没问题”的错觉。
为什么普通 boolean 就不行?
JMM 规定每个线程有自己的工作内存,对变量的读写都基于本地副本。主线程改了 flag,但没强制刷回主内存;子线程一直从自己缓存里读旧值,根本不知道变了。这不是 bug,是 JMM 的默认行为。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
立即学习“Java免费学习笔记(深入)”;
- static 只保证类级别共享,不解决缓存一致性
- final 只在构造完成时提供安全发布,不适用于运行期修改
- synchronized 虽能解决,但为单个布尔变量加锁,开销过大且易误用(比如锁对象不一致)
真正有效的三种解决方式
目标就两个:写后立即可见 + 读后必是最新的。
- volatile:最轻量级方案。修饰 flag 后,每次写自动 flush 到主内存,每次读自动 reload。适合状态标志、开关控制等单变量场景
- synchronized:哪怕只读/只写一个变量,进入临界区前清空工作内存,退出时强制写回。注意必须用同一把锁对象建立 happens-before 关系
- AtomicBoolean:底层基于 volatile + CAS,语义更明确,还支持 compareAndSet 等原子操作,适合需要条件更新的逻辑
例如:private static volatile boolean flag = true; —— 加一个 volatile,问题即解,无需改结构。
哪些做法是无效或危险的?
- 只加 static 或 public:完全不解决可见性
- 用 System.out.println() “凑巧修复”:掩盖问题,上线必崩
- 误以为 volatile int i; i++ 是线程安全的:i++ 是读-改-写三步,volatile 不保证原子性,仍会丢数据
- 在不同对象上加 synchronized:锁不统一,happens-before 不成立,可见性依旧不保
真正要验证效果,得让线程高速跑、不打断、不打印,再看行为是否符合预期——这才是逼近真实生产环境的测试方式。

















