
在Java并发编程领域,JMM(Java Memory Model,Java内存模型)是绝对的核心基石。无论是面试中高频的volatile、synchronized原理,还是生产环境中诡异的并发死循环、半初始化对象空指针、数据不一致问题,本质上都是对JMM规范的理解不到位导致的。
在理解JMM之前,我们必须先搞清楚:现代计算机架构中,并发编程到底面临什么底层问题?JMM的出现,本质上是为了统一解决这两大核心矛盾。
现代CPU的运算速度,比主内存(DRAM)的读写速度快了3-4个数量级,相当于火箭和自行车的速度差。如果CPU每次运算都直接和主内存交互,绝大多数性能都会浪费在等待内存响应上。
为了弥补这个速度鸿沟,CPU架构引入了多级高速缓存:每个CPU核心都有自己的L1、L2私有缓存,多个核心共享L3缓存,CPU运算时优先读写高速缓存,仅在必要时和主内存同步数据。
多核架构下,每个核心都有自己的私有缓存,同一个变量可能在多个核心的缓存中都存在副本。当CPU1修改了变量的缓存副本,CPU2的缓存副本还是旧值,就会出现缓存数据不一致的问题。
硬件层通过MESI等缓存一致性协议解决这个问题,但不同CPU架构(x86、ARM、RISC-V)的缓存协议、内存模型差异极大。而Java作为跨平台语言,必须给开发者提供一套统一的内存访问规范,屏蔽底层硬件和操作系统的差异,保证同一段代码在不同平台上的并发行为一致——这就是JMM诞生的核心原因。
除了缓存问题,现代CPU和编译器为了提升性能,会对指令进行重排序,分为三类:

重排序遵循as-if-serial规则:单线程环境下,无论怎么重排序,程序的执行结果不能被改变。但在多线程环境下,重排序会导致线程间的数据可见性、执行顺序出现诡异问题,这也是JMM需要规范的核心内容。
JMM是一套规范,定义了线程和主内存之间的抽象关系,核心目标是:通过规范多线程环境下变量的读写访问规则,解决多线程的原子性、可见性、有序性问题,保证Java程序在所有平台上的并发行为一致性。
这里的变量,指的是线程共享的实例字段、静态字段、数组元素,不包括线程私有的局部变量、方法参数,因为这些不会在线程间共享,不存在竞争问题。
JMM规定了所有共享变量都存储在主内存(Main Memory)中,每个线程都有自己私有的工作内存(Working Memory),工作内存中保存了该线程使用到的变量的主内存副本,线程对变量的所有读写操作,都必须在工作内存中进行,不能直接读写主内存中的变量。

JMM的工作内存≠JVM的栈内存,主内存≠JVM的堆内存,这是两个完全不同的概念:
JMM定义了8个原子操作,来完成主内存和工作内存之间的变量同步,每个操作都是不可拆分的原子操作:
JMM对这8个操作制定了严格的执行规则,核心规则包括:
并发编程的所有问题,本质上都可以归结为原子性、可见性、有序性这三大特性的问题,JMM的核心价值就是为这三大特性提供明确的保障规则。
原子性指的是:一个操作是不可中断的,要么全部执行成功,要么全部执行失败,执行过程中不会被其他线程打断。
package com.jam.demo.jmm;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 原子性问题演示
* @author ken
*/
@Slf4j
publicclass AtomicityDemo {
privatestaticvolatileint volatileCount = 0;
privatestaticfinal AtomicInteger atomicCount = new AtomicInteger(0);
privatestaticint lockCount = 0;
privatestaticfinalint THREAD_COUNT = 10;
privatestaticfinalint LOOP_COUNT = 10000;
public static void main(String[] args) throws InterruptedException {
CountDownLatch volatileLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
volatileCount++;
}
volatileLatch.countDown();
}, "volatile-thread-" + i).start();
}
volatileLatch.await();
log.info("volatile修饰的计数结果:{},预期结果:{}", volatileCount, THREAD_COUNT * LOOP_COUNT);
CountDownLatch atomicLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
atomicCount.incrementAndGet();
}
atomicLatch.countDown();
}, "atomic-thread-" + i).start();
}
atomicLatch.await();
log.info("原子类修饰的计数结果:{},预期结果:{}", atomicCount.get(), THREAD_COUNT * LOOP_COUNT);
CountDownLatch lockLatch = new CountDownLatch(THREAD_COUNT);
Object lock = new Object();
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < LOOP_COUNT; j++) {
synchronized (lock) {
lockCount++;
}
}
lockLatch.countDown();
}, "lock-thread-" + i).start();
}
lockLatch.await();
log.info("synchronized保护的计数结果:{},预期结果:{}", lockCount, THREAD_COUNT * LOOP_COUNT);
}
}
执行结果说明:
核心原因:i++不是原子操作,它分为3个步骤:
多线程环境下,多个线程可能同时读取到相同的旧值,分别执行+1运算,然后写回主内存,导致多次+1操作最终只生效一次,这就是原子性缺失导致的数据不一致问题。
可见性指的是:当一个线程修改了共享变量的值,其他线程能够立刻感知到这个修改,拿到最新的变量值。
多核CPU架构下,线程的读写操作都是针对自己的工作内存,修改后的变量副本不会立刻同步到主内存,其他线程也不会主动去主内存读取最新值,这就是可见性问题的根源。
JMM规定,所有的变量修改必须同步到主内存,变量读取必须从主内存刷新最新值,通过以下机制实现可见性保障:
package com.jam.demo.jmm;
import lombok.extern.slf4j.Slf4j;
/**
* 可见性问题演示
* @author ken
*/
@Slf4j
publicclass VisibilityDemo {
privatestaticboolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
int i = 0;
while (!flag) {
i++;
}
log.info("线程A退出循环,i的值为:{}", i);
}, "threadA");
threadA.start();
Thread.sleep(1000);
flag = true;
log.info("主线程已经将flag设置为true");
threadA.join();
}
}
执行结果说明:
核心原因: JIT编译器在高频率循环中,发现flag变量没有被当前线程修改,会进行优化,将循环条件优化为while(true),不再读取flag变量的最新值;同时,没有volatile修饰时,主线程对flag的修改不会立刻同步到主内存,线程A也不会主动从主内存刷新flag的值,最终导致死循环。
❝注意:不要在循环中添加System.out.println()来测试,println方法底层有synchronized修饰,会触发内存刷新,导致可见性问题无法复现。
有序性指的是:程序的执行顺序,按照代码的逻辑顺序执行,禁止编译器和CPU进行指令重排序。
单线程环境下,as-if-serial规则保证重排序不会改变执行结果,开发者完全感知不到。但多线程环境下,重排序会导致代码执行顺序和预期不符,出现诡异的并发问题。
DCL(Double Check Lock,双重检查锁)是开发中常用的单例实现方式,但如果没有正确使用volatile,就会因为指令重排序出现严重问题。
错误的DCL单例实现
package com.jam.demo.jmm;
/**
* 错误的DCL单例示例
* @author ken
*/
publicclass WrongDclSingleton {
privatestatic WrongDclSingleton instance;
private WrongDclSingleton() {
}
public static WrongDclSingleton getInstance() {
if (instance == null) {
synchronized (WrongDclSingleton.class) {
if (instance == null) {
instance = new WrongDclSingleton();
}
}
}
return instance;
}
}
正确的DCL单例实现
package com.jam.demo.jmm;
/**
* 正确的DCL单例示例
* @author ken
*/
publicclass CorrectDclSingleton {
privatestaticvolatile CorrectDclSingleton instance;
private CorrectDclSingleton() {
}
/**
* 获取单例实例
* @return 单例对象
*/
public static CorrectDclSingleton getInstance() {
if (instance == null) {
synchronized (CorrectDclSingleton.class) {
if (instance == null) {
instance = new CorrectDclSingleton();
}
}
}
return instance;
}
}
核心问题分析:instance = new Singleton() 这个操作,在底层分为3个步骤:
在没有volatile修饰的情况下,编译器和CPU可能会对步骤2和3进行重排序,执行顺序变为1→3→2。此时会出现以下场景:
volatile关键字通过内存屏障禁止了步骤2和3的重排序,保证对象初始化完成后,才会将引用赋值给instance变量,彻底解决了这个问题。
JMM对原子性、可见性、有序性的保障,底层核心是通过内存屏障(Memory Barrier)实现的。内存屏障是一组CPU指令,它的核心作用有两个:
JMM定义了4种标准的内存屏障,覆盖所有的读写场景:
屏障类型 | 指令示例 | 核心作用 |
|---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保证Load1的读操作,先于Load2及后续所有读操作执行 |
StoreStore | Store1; StoreStore; Store2 | 保证Store1的写操作,先于Store2及后续所有写操作执行,刷新到主内存 |
LoadStore | Load1; LoadStore; Store2 | 保证Load1的读操作,先于Store2及后续所有写操作执行 |
StoreLoad | Store1; StoreLoad; Load2 | 保证Store1的写操作,先于Load2及后续所有读操作执行,刷新到主内存,同时清空工作内存副本 |
其中,StoreLoad屏障是功能最强的屏障,它同时具备其他3种屏障的所有功能,也是开销最大的屏障,x86架构下的lock指令就是典型的StoreLoad屏障。
JMM为volatile关键字制定了严格的内存屏障插入策略,保证其可见性和有序性:
这个策略完全禁止了volatile变量的读写操作和其他操作的重排序,同时保证了volatile变量的修改会立刻刷新到主内存,读取会从主内存获取最新值。
JDK 9之后,官方推出了VarHandle类,替代了不安全的Unsafe类,提供了标准的内存屏障操作,JDK 17中已经完全稳定。VarHandle可以对变量进行细粒度的内存屏障控制,实现和volatile相同的内存语义,同时具备更高的灵活性。
package com.jam.demo.jmm;
import lombok.extern.slf4j.Slf4j;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
/**
* VarHandle内存屏障示例
* @author ken
*/
@Slf4j
publicclass VarHandleBarrierDemo {
privateint x = 0;
privatevolatileint y = 0;
privatestaticfinal VarHandle X_HANDLE;
static {
try {
X_HANDLE = MethodHandles.lookup().findVarHandle(VarHandleBarrierDemo.class, "x", int.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
thrownew ExceptionInInitializerError(e);
}
}
public void write() {
x = 10;
VarHandle.storeStoreFence();
y = 1;
}
public void read() {
if (y == 1) {
VarHandle.loadLoadFence();
log.info("x的值为:{}", x);
}
}
public static void main(String[] args) throws InterruptedException {
VarHandleBarrierDemo demo = new VarHandleBarrierDemo();
for (int i = 0; i < 10000; i++) {
Thread writer = new Thread(demo::write, "writer-thread-" + i);
Thread reader = new Thread(demo::read, "reader-thread-" + i);
writer.start();
reader.start();
writer.join();
reader.join();
demo.x = 0;
demo.y = 0;
}
log.info("测试完成,所有读操作都能正确获取x的值");
}
}
代码说明:
VarHandle.storeStoreFence()禁止x的写操作和y的写操作重排序,保证x的修改先于y的修改完成VarHandle.loadLoadFence()保证y的读操作先于x的读操作执行,只要y等于1,一定能读到x的最新值10fullFence()方法,对应StoreLoad屏障,实现全功能的内存屏障控制很多开发者对JMM的理解,停留在volatile、synchronized的关键字层面,但实际上,JMM的核心是Happens-Before先行发生原则,它是判断多线程环境下数据是否可见、操作是否有序的唯一标准。
首先必须纠正一个90%的人都误解的点:Happens-Before不是说A操作在B操作之前执行,而是A操作的执行结果,对B操作完全可见。
JMM定义:如果操作A Happens-Before 操作B,那么A操作的结果对B操作可见,且A操作的执行顺序排在B操作之前。
Happens-Before具备天然的传递性:如果A Happens-Before B,B Happens-Before C,那么A Happens-Before C。
JMM定义了8条天然的Happens-Before规则,无需任何同步操作,这些规则天然生效,是并发编程中可见性和有序性的核心依据。
在一个线程内,按照控制流顺序,前面的操作Happens-Before于后面的任意操作。
❝注意:是控制流顺序,不是代码顺序,if、for、switch等分支结构,会遵循实际执行的控制流顺序。
对同一个锁的unlock操作,Happens-Before于后面对这个锁的lock操作。
❝这是synchronized和Lock的可见性、有序性保障的核心依据,前一个线程解锁前的所有修改,对后一个线程加锁后的所有操作完全可见。
对一个volatile变量的写操作,Happens-Before于后面对这个变量的读操作。
❝这是volatile可见性、有序性保障的核心依据,写volatile的所有结果,对后续读volatile的操作完全可见。
Thread对象的start()方法,Happens-Before于此线程内的所有操作。
❝主线程启动子线程前的所有修改,对子线程启动后的所有操作完全可见。
一个线程内的所有操作,Happens-Before于其他线程对这个线程的终止检测。
❝子线程执行完成后的所有修改,主线程通过Thread.join()等待子线程终止、Thread.isAlive()检测到子线程终止后,完全可见。
对线程interrupt()方法的调用,Happens-Before于被中断线程检测到中断事件的发生。
❝其他线程调用interrupt()中断线程前的所有修改,被中断线程检测到中断后,完全可见。
一个对象的初始化完成(构造方法执行结束),Happens-Before于它的finalize()方法的开始。
❝保证对象finalize()方法执行时,一定能看到对象初始化完成后的所有字段值。
如果操作A Happens-Before 操作B,操作B Happens-Before 操作C,那么操作A Happens-Before 操作C。
❝这是Happens-Before规则的核心扩展能力,通过传递性,可以组合多条规则,实现复杂场景的可见性保障。
package com.jam.demo.jmm;
import lombok.extern.slf4j.Slf4j;
/**
* Happens-Before规则演示
* @author ken
*/
@Slf4j
publicclass HappensBeforeDemo {
privatestaticint a = 0;
privatestaticvolatileint b = 0;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
a = 10;
b = 1;
}, "threadA");
Thread threadB = new Thread(() -> {
if (b == 1) {
log.info("a的值为:{}", a);
}
}, "threadB");
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
}
规则分析:
a=10 Happens-Before b=1b=1的写操作,Happens-Before threadB中b==1的读操作a=10 Happens-Before b=1,b=1 Happens-Before b==1的读操作,因此a=10 Happens-Before threadB中读取a的操作因此,只要threadB中读到b等于1,就一定能读到a的值为10,不会出现a为0的情况,这就是Happens-Before规则的核心作用。
volatile是JMM提供的最轻量级的同步机制,很多开发者只知道它能保证可见性,却不知道它的完整内存语义:
volatile的适用场景:
synchronized是JVM内置的锁机制,也是开发中最常用的同步手段,它的内存语义是全方面的:
synchronized的底层通过对象头的Mark Word实现锁升级(无锁→偏向锁→轻量级锁→重量级锁),JDK 17中对synchronized做了大量优化,性能已经和Lock接口非常接近,开发中优先使用synchronized,降低代码复杂度。
JSR-133规范对final的内存语义做了大幅增强,保证了final字段的初始化安全:
核心结论:只要对象是正确构造的(构造方法执行完成后,才将对象引用发布给其他线程,没有this溢出),那么其他线程无需任何同步操作,就能看到final字段的正确初始化值。
package com.jam.demo.jmm;
import lombok.extern.slf4j.Slf4j;
/**
* final字段this溢出错误示例
* @author ken
*/
@Slf4j
publicclass FinalEscapeDemo {
privatefinalint value;
privatestatic FinalEscapeDemo instance;
public FinalEscapeDemo() {
instance = this;
this.value = 10;
}
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(FinalEscapeDemo::new, "writer-thread");
Thread reader = new Thread(() -> {
if (instance != null) {
log.info("value的值为:{}", instance.value);
}
}, "reader-thread");
writer.start();
reader.start();
writer.join();
reader.join();
}
}
问题分析: 构造方法中,在final字段初始化前,就将this引用发布给了静态变量instance,其他线程拿到instance对象时,final字段value可能还没有完成初始化,会读到默认值0,即使value是final修饰的。
正确做法:在构造方法执行完成后,再发布对象引用,避免this溢出。
CPU缓存是以缓存行(Cache Line)为单位存储的,主流CPU的缓存行大小是64字节,一个long类型占8字节,因此一个缓存行可以存储8个long类型的变量。
当多个线程同时修改同一个缓存行中的不同变量时,即使这些变量之间没有任何关联,也会因为缓存一致性协议,导致缓存行频繁失效,性能急剧下降,这就是伪共享(False Sharing)问题。
伪共享是高并发场景下的隐形性能杀手,很多并发框架(Disruptor、Netty)都针对伪共享做了专门的优化。
package com.jam.demo.jmm;
import lombok.extern.slf4j.Slf4j;
import jdk.internal.vm.annotation.Contended;
/**
* 伪共享问题演示与解决方案
* @author ken
*/
@Slf4j
publicclass FalseSharingDemo {
privatestaticfinalint THREAD_COUNT = 2;
privatestaticfinallong LOOP_COUNT = 1000_000_000L;
staticclass NoPaddingObject {
publicvolatilelong value = 0L;
}
staticclass ManualPaddingObject {
publicvolatilelong value = 0L;
publiclong p1, p2, p3, p4, p5, p6, p7;
}
staticclass ContendedObject {
@Contended
publicvolatilelong value = 0L;
}
public static void main(String[] args) throws InterruptedException {
NoPaddingObject[] noPaddingObjects = new NoPaddingObject[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
noPaddingObjects[i] = new NoPaddingObject();
}
long start = System.currentTimeMillis();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
int index = i;
threads[i] = new Thread(() -> {
for (long j = 0; j < LOOP_COUNT; j++) {
noPaddingObjects[index].value = j;
}
}, "no-padding-thread-" + i);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long end = System.currentTimeMillis();
log.info("无填充的执行时间:{}ms", end - start);
ManualPaddingObject[] manualPaddingObjects = new ManualPaddingObject[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
manualPaddingObjects[i] = new ManualPaddingObject();
}
start = System.currentTimeMillis();
threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
int index = i;
threads[i] = new Thread(() -> {
for (long j = 0; j < LOOP_COUNT; j++) {
manualPaddingObjects[index].value = j;
}
}, "manual-padding-thread-" + i);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
end = System.currentTimeMillis();
log.info("手动填充的执行时间:{}ms", end - start);
ContendedObject[] contendedObjects = new ContendedObject[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
contendedObjects[i] = new ContendedObject();
}
start = System.currentTimeMillis();
threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
int index = i;
threads[i] = new Thread(() -> {
for (long j = 0; j < LOOP_COUNT; j++) {
contendedObjects[index].value = j;
}
}, "contended-thread-" + i);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
end = System.currentTimeMillis();
log.info("@Contended注解的执行时间:{}ms", end - start);
}
}
执行结果说明:
解决方案说明:
--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED才能使用。本文所有示例代码,均基于JDK 17:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jam.demo</groupId>
<artifactId>jmm-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>jmm-demo</name>
<description>JMM内存模型实战示例项目</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.34</lombok.version>
<spring.version>6.1.14</spring.version>
<fastjson2.version>2.0.53</fastjson2.version>
<guava.version>33.2.1-jre</guava.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<springdoc.version>2.6.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.3.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
JMM是Java并发编程的绝对基石,它的核心价值是通过一套统一的规范,屏蔽底层硬件和操作系统的内存模型差异,为Java开发者提供一致的并发编程语义。本文从硬件底层原理出发,完整讲解了JMM的抽象结构、三大核心特性、内存屏障底层实现、Happens-Before核心规则,结合可运行的实战代码,拆解了开发中高频的踩坑点,最终给出了生产环境的最佳实践。 理解JMM,本质上是理解多线程环境下,共享变量的读写规则,只有彻底吃透这些规则,才能写出正确、高效的并发代码,从根源上避免诡异的并发问题。