Java 共享变量的内存可见性问题
- [Java 共享变量的内存可见性问题](#Java 共享变量的内存可见性问题)
- 为什么会出现共享变量的内存可见性问题
- 如何解决共享变量的内存可见性问题
Java 共享变量的内存可见性问题
共享变量的内存可见性是多线程编程场景下的核心内存一致性问题,具体指:当一个线程对存储在主内存中的共享变量执行写操作后,该修改能否被其他访问此变量的线程及时、准确地感知到 ------ 若其他线程无法立即读取到最新值,仍获取到修改前的过期值,则称存在内存可见性问题;反之,若修改能被所有线程实时感知,则称该变量具备内存可见性。
为什么会出现共享变量的内存可见性问题
内存可见性问题的产生核心原因包括Java 内存模型的抽象设计(理论基础)和 CPU 的高速缓存性能优化机制(硬件落地支撑),前者定义了线程操作共享变量的规则边界,后者放大了数据不一致的问题,两者共同导致了可见性问题的出现。
一、理论基础:Java 内存模型的抽象设计
Java 内存模型(Java Memory Model,JMM)是一套抽象的内存访问规范,并非实际存在的内存区域。它的核心目的是屏蔽不同硬件、操作系统的内存访问差异,保证 Java 程序在不同平台上具有一致的内存行为,同时为多线程并发操作提供基础的内存语义约束。
JMM 的核心抽象是将内存划分为主内存和工作内存,两者的职责、存储内容和交互规则,是可见性问题产生的根本理论根源。
主内存(Main Memory):所有线程的共享数据存储区
- 内存区域:包括 JVM 堆内存和方法区;
- 存储内容:所有共享变量均存储在主内存中,具体包括类的实例变量、静态变量、数组元素;
- 核心特性:全局共享、线程可访问,但线程无法直接操作主内存中的共享变量,所有对共享变量的操作必须通过工作内存中转完成。
工作内存(Working Memory):单个线程的私有操作区
- 存储内容:线程私有数据以及该线程需要操作的共享变量副本(从主内存中加载的快照);
- 对应 JVM 实际内存区域:工作内存对应 JVM 虚拟机栈和程序计数器,其中共享变量副本的存储逻辑会映射到 CPU 缓存 / 寄存器;
- 核心特性与规则:
私有性:每个线程都有独立的工作内存,其他线程无法直接访问或修改当前线程工作内存中的数据;
操作独占性:线程对共享变量的所有读写操作,必须在自身的工作内存中进行,禁止直接操作主内存的共享变量;
中转通信性:线程间的共享变量值传递,必须通过主内存完成中转;
私有变量无可见性问题:方法内的局部变量、方法参数、异常参数等,仅存储在当前线程的虚拟机栈中,不进入主内存,也不存在跨线程共享的属性,因此不会产生内存可见性问题。

JMM 明确了线程操作共享变量的两个核心步骤,且对写回主内存的时机未做强制即时约束,这是可见性问题的关键:
- 读取共享变量:线程在操作共享变量前,必须先从主内存中将该变量的最新值加载到自身工作内存,创建一份私有副本,后续所有操作均基于该副本,不再主动同步主内存的变化(除非触发重新加载)。
- 修改共享变量:线程修改工作内存中的共享变量副本后,仅会先更新该副本的值,再在某个不确定的时机将修改后的副本写回主内存,更新主内存中的原始值。
这里的核心关键点是 JMM 未要求线程修改共享变量副本后立即写回主内存,这就导致了两个层面的数据不一致:
主内存中的共享变量原始值,与修改线程工作内存中的副本值不一致(未及时写回);
其他线程工作内存中的共享变量副本,与主内存中的最新值不一致(未及时重新加载);
最终表现为当一个线程修改了共享变量,其他线程无法及时感知到该修改,依然读取到旧值,即出现内存可见性问题。
二、硬件支撑:CPU 高速缓存优化(可见性问题的实际落地与放大)
JMM 中的工作内存是抽象概念,其实际硬件载体是 CPU 的高速缓存和寄存器。现代 CPU 为了弥补主内存读写速度与 CPU 运算速度的巨大差距,引入了高速缓存优化机制,这一机制既是 JMM 工作内存的落地实现,也进一步放大了内存可见性问题。

高速缓存的分级结构与 JMM 的映射关系
如图所示,典型的多核 CPU 架构中,高速缓存分为 3 级(L1、L2、L3),各级缓存的性能、容量和共享范围存在明确差异,且与 JMM 的内存模型形成精准映射:
| 缓存级别 | 所属范围 | 访问速度 | 存储容量 | 与 JMM 的映射关系 |
|---|---|---|---|---|
| L1 缓存 | 单个 CPU 核心私有 | 最快 | 最小(几 KB 到几十 KB) | 主要映射 JMM 工作内存,存储线程频繁操作的共享变量副本 |
| L2 缓存 | 单个 CPU 核心私有 | 次快 | 中等(几十 KB 到几百 KB) | 辅助映射 JMM 工作内存,作为 L1 缓存与 L3 缓存的缓冲 |
| L3 缓存 | 所有 CPU 核心共享 | 较慢 | 较大(几 MB 到几十 MB) | 介于主内存与 L2 缓存之间,减少多核核心对主内存的直接访问 |
高速缓存机制如何放大内存可见性问题:
- 优先读写缓存,弱化主内存交互:CPU 执行线程指令时,会优先从 L1→L2→L3 缓存中查找所需数据,只有在缓存未命中时,才会从主内存中加载数据并写入缓存;同理,CPU 修改缓存中的数据后,不会立即将其同步回主内存;
- 缓存同步的触发条件苛刻:缓存中的数据只有在满足特定条件时,才会被同步回主内存,常见触发条件包括:对应缓存行被淘汰(缓存空间不足,按照 LRU 等算法淘汰不常用数据),触发缓存一致性协议的同步规则,CPU 执行内存屏障指令;
- 多核架构下的缓存孤岛问题:在多核 CPU 架构中,每个核心都有私有 L1/L2 缓存。当多个线程分别运行在不同核心上,且操作同一个共享变量时,该变量会被加载到多个核心的私有缓存中,形成缓存孤岛。其中一个核心修改了缓存中的变量值并同步回主内存后,其他核心的私有缓存中依然保留着旧值,且不会主动刷新,直到该核心的缓存行被淘汰或触发同步规则,这就进一步加剧了数据不一致的问题。
补充:缓存一致性协议的局限性
为了解决多核缓存的孤岛问题,硬件层面引入了缓存一致性协议,其核心作用是:当某个核心修改了缓存中的共享变量值并同步回主内存时,会通知其他核心失效对应缓存行中的旧值,迫使其他核心重新从主内存加载最新值。
但需要注意的是,缓存一致性协议仅能解决缓存与主内存的数据同步问题,无法解决 JMM 层面的延迟写回问题等。因此,即使有缓存一致性协议,依然无法从根本上消除内存可见性问题,还需要依赖 Java 层面的关键字或同步机制来补充约束。
代码示例:
java
public class Test {
public static Boolean flag = false;
public static void main(String[] args) throws InterruptedException {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
new Thread(threadA, "threadA").start();
Thread.sleep(1000l);//为了保证threadA比threadB先启动
new Thread(threadB, "threadB").start();
}
static class ThreadA implements Runnable {
public void run() {
while (true) {
if (flag) {
System.out.println(Thread.currentThread().getName() + " : flag is " + flag);
break;
}
}
}
}
static class ThreadB implements Runnable {
public void run() {
flag = true;
System.out.println(Thread.currentThread().getName() + " : flag is " + flag);
}
}
}

线程 B 成功将flag修改为true并打印日志,但线程 A 会无限循环,永远不会停止,也不会打印自身的日志。
这段代码出现了内存一致性问题。代码中定义了静态共享变量flag,线程 A 先启动并进入无限循环,持续检测flag是否为true,线程 B 延迟 1 秒启动后将flag修改为true并打印日志,但运行后会发现,线程 B 能正常完成修改和日志打印,而线程 A 会一直无限循环无法停止,也不会打印自身的日志,这就是内存一致性问题的直观体现;底层原因是按照 Java 内存模型的规定,线程 A 启动后会从主内存加载flag的初始值false到自身私有工作内存,之后的循环中仅读取工作内存中的该旧副本,不会主动去主内存刷新最新值,而线程 B 修改flag为true后,虽会将修改后的值写回主内存,但该修改结果对线程 A 的工作内存是不可见的,线程 A 无法感知到flag已被修改为true,依然持续读取工作内存中的旧值false,这种因共享变量修改无法被其他线程及时感知、导致多线程数据不一致的情况,就是典型的内存一致性问题中的内存可见性问题。
如何解决共享变量的内存可见性问题
synchronized和volatile都可以解决上面代码的内存可见性问题,但两者的解决原理、实现方式和轻量化程度有显著区别:
- volatile是专门针对可见性问题的轻量化解决方案,直接修饰共享变量flag,通过 CPU 内存屏障指令强制修改后的值立即刷新回主内存,并通知其他线程失效旧副本,同时强制读取时从主内存加载最新值,以此保证可见性,修改方式简单且无额外锁开销,非常贴合这段代码仅需解决可见性的场景;
- 而synchronized是通过锁机制实现的重量级解决方案,需要将线程 A 中读取flag的判断逻辑、线程 B 中修改flag的逻辑都包裹在同一个共享锁对象的同步代码块中,利用线程获取锁时清空工作内存旧副本、从主内存加载最新值,释放锁时将修改后的值强制写回主内存的附带内存语义,间接保证可见性,同时synchronized还能保证原子性和有序性,但对于这段仅需解决可见性的简单场景来说,存在功能冗余且并发性能略低于volatile。
这两个 JVM 关键字会在我后面的博客里详细介绍。