《Java 内存模型(JMM)与 volatile、synchronized 可见性原理》


一、前言:为什么需要 Java 内存模型(JMM)

在多线程环境下,每个线程都有自己的工作内存(CPU缓存),
而共享变量存放在主内存中。

问题是:

一个线程修改变量后,其他线程可能看不到最新值。

原因:

  • CPU 缓存导致“可见性问题”;
  • 编译器和 CPU 指令优化导致“重排序问题”;
  • 缺乏同步导致“原子性问题”。

于是,JVM 定义了 Java 内存模型(Java Memory Model, JMM)
用来规定多线程访问共享变量时的行为规范,
即:什么时候写入主内存、什么时候刷新到工作内存、哪些操作是有序的。


二、JMM 内存结构与线程交互过程

(1)整体结构示意
               ┌──────────────────────────────┐
               │          主内存(Main Memory)│
               │    共享变量、对象实例、静态区 │
               └───────────┬──────────────────┘
                           │
         ┌────────────────┴────────────────┐
         │                                 │
┌──────────────────────┐        ┌──────────────────────┐
│   线程 A 的工作内存   │        │   线程 B 的工作内存   │
│   寄存器 + CPU 缓存   │        │   寄存器 + CPU 缓存   │
└──────────────────────┘        └──────────────────────┘

每个线程:

  • 从主内存读取变量副本到工作内存;
  • 修改后再写回主内存;
  • 线程间 不直接共享工作内存
(2)JMM 规定的 8 种原子操作

JMM 定义了主内存与工作内存的交互规则:

操作 含义
lock 锁定变量,标识为线程独占
unlock 解锁变量,使其他线程可见
read 从主内存读取变量值
load 将值载入工作内存
use 执行代码时使用变量
assign 给变量赋值
store 将变量值写入主内存副本
write 将 store 的值写回主内存

这些操作保证多线程访问的可见性与顺序性。


三、happens-before 原则(JMM 的核心规则)

JMM 并不强制“物理执行顺序”,
而是通过 happens-before 原则 定义“逻辑可见性关系”。

happens-before 规则 含义
程序顺序规则 单线程内,代码按顺序执行
监视器锁规则 对同一锁的 unlock 操作先行于后续的 lock
volatile 变量规则 对 volatile 的写先行于后续对它的读
线程启动规则 Thread.start() 先行于线程体内的操作
线程终止规则 线程结束(join)先行于 join 返回
传递性规则 A → B, B → C ⇒ A → C

举例:

int a = 0;
boolean flag = false;

Thread t1 = new Thread(() -> {
    a = 1;
    flag = true;
});

Thread t2 = new Thread(() -> {
    if (flag) System.out.println(a);
});

flagvolatile,则:

t1a 的写入 happens-before t2flag 的读取。
保证 t2 看到 a=1,而不是旧值。


四、volatile 关键字详解

(1)volatile 的作用
  • 保证可见性:修改立即刷新到主内存;
  • 禁止指令重排:在读写操作前后插入内存屏障;
  • 不保证原子性:非复合操作仍可能被打断。
(2)底层实现(内存屏障)

JIT 编译后,volatile 读写会插入内存屏障指令(Memory Barrier):

屏障类型 位置 作用
StoreStoreBarrier 写操作前 保证写入主内存前先完成前序写入
StoreLoadBarrier 写操作后 禁止写后读乱序
LoadLoadBarrier 读操作前 保证读取最新值
LoadStoreBarrier 读操作后 防止读后写乱序

JVM 通过这些屏障确保“先写再读”的可见性与顺序性。

(3)volatile 的局限
volatile int count = 0;
count++; // 非原子操作

count++ 实际包含三步:

  1. 读取 count;
  2. 自增;
  3. 写回内存。
    → 线程切换可能导致值覆盖,仍需使用 AtomicIntegersynchronized

五、synchronized 与内存可见性

(1)锁的语义

synchronized 同步块在 JVM 层面基于 Monitor 实现。
进入同步块相当于:

lock + read/load + use + assign + store/write + unlock

(2)内存可见性

进入 synchronized

  • 线程会从主内存刷新共享变量;
  • 执行结束后将修改写回主内存;
  • 确保锁内代码对后续线程可见。
(3)与 volatile 的区别
特性 volatile synchronized
可见性 ✅ 保证 ✅ 保证
原子性 ❌ 不保证 ✅ 保证
有序性 ✅ 保证 ✅ 保证
实现方式 内存屏障 Monitor 锁
性能 轻量 重量(可优化为偏向/轻量级锁)

六、面试高频问题与答题模板

问题 答案要点
Q1:JMM 是什么? Java Memory Model,定义多线程读写共享变量的可见性和顺序性规范。
Q2:JMM 为什么存在? 为屏蔽 CPU 缓存与指令重排带来的并发不一致问题。
Q3:volatile 解决了什么问题? 保证可见性与有序性,不保证原子性。
Q4:synchronized 如何保证可见性? 通过进入/退出 monitor 实现内存同步(刷新主内存)。
Q5:volatile 底层实现? 通过内存屏障指令禁止重排序(StoreLoadBarrier)。
Q6:happens-before 是什么? 定义逻辑上的可见性顺序,不等同于物理执行顺序。
Q7:volatile 适用于哪些场景? 状态标志、单例双重校验锁、轻量读写控制。

结语

JMM 是理解所有并发机制的理论根基。
volatilesynchronized,再到 AQSCASLockSupport
它们的本质都是围绕三件事展开:

可见性、有序性、原子性。

掌握 JMM,你就能从“语法层并发”跃升到“CPU 层理解”,
真正能解释为什么一行 volatile 能让系统稳定运行。

下一篇,我将写——
《Redis 高并发八股文:缓存穿透、击穿、雪崩与解决方案》
进入高并发实战篇,讲清 Redis 三大核心问题的区别、原理与企业级防护方案。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐