Java内存模型JMM深度解析happens-before规则与volatile原理在 Java 并发编程中开发者最常遇到但又最难以理解的概念就是 Java 内存模型JMM。为什么多线程下会出现数据不一致volatile到底如何保证可见性happens-before规则与 “代码重排序” 有什么关系本文将从底层原理出发结合代码实例彻底讲透 JMM 的核心机制。一、为什么需要 JMM从硬件架构说起1.1 现代计算机的多级存储架构┌─────────────────┐ │ 寄存器 (L0) │ 1ns, 容量极小 │ CPU 核心私有 │ ├─────────────────┤ │ L1 缓存 │ ~1-2ns, 32KB │ CPU 核心私有 │ (Harvard 结构数据缓存 指令缓存) ├─────────────────┤ │ L2 缓存 │ ~3-10ns, 256KB-512KB │ CPU 核心私有 │ ├─────────────────┤ │ L3 缓存 │ ~10-20ns, 8-32MB │ 多核共享 │ ├─────────────────┤ │ 主内存 (RAM) │ ~80-100ns, 16-128GB │ 所有 CPU 共享 │ └─────────────────┘在多核 CPU 架构下每个核心有自己的 L1/L2 缓存。线程 A 在 Core 1 修改了变量线程 B 在 Core 2 读取时可能读取的是旧缓存值而非最新内存值。1.2 缓存一致性问题经典示例publicclassCacheVisibilityProblem{// ❌ 没有 volatile可能永远循环privatestaticbooleanrunningtrue;// ✅ 使用 volatile 保证可见性// private static volatile boolean running true;publicstaticvoidmain(String[]args)throwsException{ThreadworkernewThread(()-{while(running){// 空循环可能被 JIT 优化后缓存到寄存器}System.out.println(Worker 线程已停止);});worker.start();Thread.sleep(1000);System.out.println(主线程设置 running false);runningfalse;// 对 worker 线程可能不可见worker.join(5000);// 等待 5 秒System.out.println(Worker 存活状态: worker.isAlive());}}运行结果可能主线程设置 running false Worker 存活状态: true // 程序未结束worker 仍在死循环原因主线程修改的running false可能只写入了主线程的本地缓存而 worker 线程读取的是自己的缓存副本永远为true。二、JMM 抽象模型主内存与工作内存2.1 JMM 定义Java 内存模型Java Memory Model, JMM定义了一套抽象的内存规范屏蔽了底层硬件和操作系统的差异┌─────────────────────────────────────────────────────┐ │ 主内存 (Main Memory) │ │ 所有共享变量存储于此 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ variable │ │ variable │ │ variable │ │ │ │ x │ │ y │ │ z │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ 线程 A │ │ 线程 B │ │ │ │ 工作内存 │ │ 工作内存 │ │ │ │ ┌──────┐ │ │ ┌──────┐ │ │ │ │ │ x副本 │ │ │ │ x副本 │ │ │ │ │ │ y副本 │ │ │ │ y副本 │ │ │ │ │ └──────┘ │ │ └──────┘ │ │ │ │ - read │ │ - read │ │ │ │ - load │ │ - load │ │ │ │ - use │ │ - use │ │ │ │ - assign│ │ - assign│ │ │ │ - store │ │ - store │ │ │ │ - write │ │ - write │ │ │ └──────────┘ └──────────┘ │ └─────────────────────────────────────────────────────┘JMM 规定所有变量都存储在主内存每个线程有自己的工作内存抽象概念对应 L1/L2 缓存和寄存器线程对变量的操作必须在工作内存中进行不能直接读写主内存。2.2 内存交互的 8 种操作JMM 定义了以下原子操作操作作用说明lock主内存变量锁定变为线程独占unlock主内存变量解锁释放后其他线程可锁定read主内存 → 工作内存读取变量值到线程工作内存load工作内存将 read 的值放入变量副本use工作内存 → 执行引擎使用变量值给执行引擎assign执行引擎 → 工作内存将执行结果赋值给变量副本store工作内存 → 主内存将变量副本写入主内存write主内存将 store 的值写入变量read 和 load、store 和 write 必须成对出现但中间可以插入其他指令。三、happens-before 规则并发可见性的保证3.1 什么是 happens-beforehappens-before先行发生是 JMM 中定义偏序关系的核心概念如果 A happens-before B那么 A 的结果对 B 可见且 A 在 B 之前执行。注意happens-before 不是时间上的先后而是语义上的保证——即使 A 的实际执行在 B 之后重排序后只要存在 happens-before 关系B 就必须看到 A 的结果。3.2 JMM 的 8 条 happens-before 规则publicclassHappensBeforeDemo{// 规则1程序次序规则Program Order Rule// 同一个线程内前面的操作 happens-before 后面的操作publicvoidrule1(){inta1;// 操作 Aintba1;// 操作 BA happens-before B}// 规则2监视器锁规则Monitor Lock Rule// 解锁 happens-before 后续的加锁privatefinalObjectlocknewObject();privateintsharedVar0;publicvoidrule2_write(){synchronized(lock){sharedVar1;// 解锁前写入}}publicintrule2_read(){synchronized(lock){returnsharedVar;// 加锁后读取保证看到 1}}// 规则3volatile 变量规则Volatile Variable Rule// volatile 写 happens-before 后续的 volatile 读privatevolatileintvolatileVar0;publicvoidrule3_write(){volatileVar1;// volatile 写}publicintrule3_read(){returnvolatileVar;// 后续 volatile 读保证看到 1}// 规则4线程启动规则Thread Start Rule// Thread.start() happens-before 线程内的所有操作publicvoidrule4(){intvalue42;ThreadthreadnewThread(()-{// 一定能看到 value 42System.out.println(value);});value42;// 这一行必须在 start() 之前thread.start();}// 规则5线程终止规则Thread Termination Rule// 线程内的所有操作 happens-before 从 Thread.join() 返回publicvoidrule5()throwsException{int[]resultnewint[1];ThreadworkernewThread(()-{result[0]42;// 线程内操作});worker.start();worker.join();// 返回后一定能看到 result[0] 42System.out.println(result[0]);}// 规则6中断规则Interruption Rule// 对线程 interrupt() 调用 happens-before 检测到中断事件publicvoidrule6(){ThreadworkernewThread(()-{while(!Thread.currentThread().isInterrupted()){// 当主线程调用 interrupt()isInterrupted() 一定能检测到}});worker.start();worker.interrupt();// 中断 happens-before 检测到}// 规则7终结器规则Finalizer Rule// 对象的构造函数执行 happens-before finalize() 方法// 规则8传递性Transitivity// A happens-before B且 B happens-before C则 A happens-before Cprivatevolatileinta0;privateintb0;publicvoidrule8_write(){b1;// 操作 Aa1;// 操作 Bvolatile 写}publicvoidrule8_read(){inttempAa;// 操作 Cvolatile 读inttempBb;// 操作 D// 由于 A 在 B 之前同线程B 在 C 之前volatile 规则// 由传递性A happens-before C所以 D 一定能看到 b 1}}3.3 happens-before 规则速查表规则关键描述代码标志程序次序同线程内的代码顺序无监视器锁unlock→lock后续synchronizedvolatilevolatile 写→volatile 读volatile线程启动start()→ 线程内操作Thread.start()线程终止线程内操作 →join()返回Thread.join()中断interrupt()→ 检测中断interrupt()终结器构造 →finalize()构造函数传递性A→B, B→C ⟹ A→C组合使用四、volatile 的底层原理可见性与有序性4.1 volatile 保证什么volatile 是 JVM 提供的最轻量级同步机制它保证两个语义特性保证说明可见性✅一个线程修改 volatile 变量其他线程立即可见有序性✅禁止指令重排序确保执行顺序符合预期原子性❌复合操作如 i不保证原子4.2 volatile 的内存语义publicclassVolatileMemorySemantics{privatevolatileintvolatileVar0;privateintnormalVar0;publicvoidwrite(){normalVar1;// 操作 A普通写volatileVar1;// 操作 Bvolatile 写// volatile 写之前的代码不能重排序到之后}publicvoidread(){inttempvolatileVar;// 操作 Cvolatile 读intnormalnormalVar;// 操作 D普通读// volatile 读之后的代码不能重排序到之前// 且由于 happens-before 传递性D 一定能看到 A 的结果}}volatile 内存屏障Memory Barrier插入规则volatile 写在写之后插入StoreStore和StoreLoad屏障volatile 读在读之前插入LoadLoad和LoadStore屏障volatile 写屏障 普通写 [StoreStore屏障] // 禁止前面的普通写与 volatile 写重排序 volatile 写 [StoreLoad屏障] // 禁止 volatile 写与后面的读写重排序 volatile 读屏障 [LoadLoad屏障] // 禁止 volatile 读与后面的读重排序 [LoadStore屏障] // 禁止 volatile 读与后面的写重排序 volatile 读 普通读/写4.3 代码验证volatile 的可见性publicclassVolatileVisibilityTest{privatevolatilebooleanflagfalse;privateintvalue0;publicvoidwriter(){value42;// 普通写flagtrue;// volatile 写value42 对 reader 可见}publicvoidreader(){while(!flag){// volatile 读Thread.yield();}// 由于 happens-before 传递性value 一定为 42System.out.println(value value);}publicstaticvoidmain(String[]args){VolatileVisibilityTesttestnewVolatileVisibilityTest();ThreadwriternewThread(test::writer);ThreadreadernewThread(test::reader);reader.start();writer.start();}}4.4 volatile 不保证原子性publicclassVolatileNotAtomic{privatevolatileintcount0;// i 是三步操作读取、1、写回不是原子的publicvoidincrement(){count;// 非原子操作多线程下会丢失更新}publicstaticvoidmain(String[]args)throwsException{VolatileNotAtomictestnewVolatileNotAtomic();Thread[]threadsnewThread[100];for(inti0;i100;i){threads[i]newThread(()-{for(intj0;j1000;j){test.increment();}});threads[i].start();}for(Threadt:threads)t.join();// 预期 100000实际可能远小于此值System.out.println(count test.count);}}解决方案使用AtomicInteger或synchronized// ✅ 使用 AtomicIntegerprivateAtomicIntegeratomicCountnewAtomicInteger(0);publicvoidsafeIncrement(){atomicCount.incrementAndGet();// CAS 原子操作}// ✅ 使用 synchronizedprivateintsyncCount0;publicsynchronizedvoidsafeIncrement2(){syncCount;}五、指令重排序与内存屏障5.1 重排序的三种类型重排序类型发生阶段说明示例编译器重排序编译器JIT 优化指令顺序编译器调整无关语句顺序指令级重排序CPU处理器乱序执行现代 CPU 的流水线执行内存系统重排序内存缓存/缓冲导致的可见乱序写缓冲区延迟写入5.2 数据依赖性重排序的边界编译器和处理器不会改变数据依赖性的指令顺序// 写后读依赖不能重排序inta1;// 写 aintba1;// 读 a依赖上一行// 写后写依赖不能重排序inta1;// 写 ainta2;// 再次写 a// 读后写依赖不能重排序intba;// 读 ainta1;// 写 a注意数据依赖性只针对单线程和单个处理器内的执行。多线程之间的数据依赖性不被考虑这正是 JMM 需要解决的问题。5.3 as-if-serial 语义JMM 和编译器保证单线程程序的执行结果永远不会改变无论怎么重排序。这称为 as-if-serial 语义。// 单线程下编译器可以重排序inta1;// 操作 Aintb2;// 操作 B与 A 无关可重排序到 A 之前intcab;// 操作 C依赖 A 和 B不能重排序// 多线程下没有 happens-before 保证重排序可能导致问题5.4 双重检查锁定DCL问题publicclassDoubleCheckedLocking{// ❌ 错误未加 volatileprivatestaticDoubleCheckedLockinginstance;// ✅ 正确加 volatile 禁止重排序// private static volatile DoubleCheckedLocking instance;privateintvalue;// 初始化值privateDoubleCheckedLocking(){this.value42;// 操作 A}publicstaticDoubleCheckedLockinggetInstance(){if(instancenull){// 第一次检查无锁synchronized(DoubleCheckedLocking.class){if(instancenull){// 第二次检查加锁instancenewDoubleCheckedLocking();// 危险new 操作可能重排序为// 1. 分配内存// 2. 将引用指向内存地址instance 非 null// 3. 初始化对象value 42// 如果步骤 2 和 3 重排序其他线程可能拿到未初始化对象}}}returninstance;}}解决方案必须使用volatile修饰instanceprivatestaticvolatileDoubleCheckedLockinginstance;volatile 的StoreStore和StoreLoad屏障会禁止步骤 2 和 3 的重排序确保对象完全初始化后才暴露引用。六、volatile vs synchronized 对比特性volatilesynchronized可见性✅✅原子性❌仅单读写✅有序性✅禁止重排序✅阻塞❌ 不阻塞✅ 会阻塞适用场景状态标志、单次读写复合操作、临界区性能轻量级仅内存屏障重量级Monitor典型用法boolean flagi、多步骤操作// ✅ volatile 适合状态标志privatevolatilebooleanrunningtrue;// ✅ synchronized 适合复合操作privateintcounter0;publicsynchronizedvoidincrementAndGet(){counter;if(counter100){counter0;// 多步操作需要原子性}}七、避坑指南7.1 ⚠️ 坑 1误认为 volatile 保证原子性// ❌ 错误volatile 不能保证 i 原子privatevolatileintcount0;publicvoidincrement(){count;}// 非原子// ✅ 正确使用 AtomicIntegerprivateAtomicIntegercountnewAtomicInteger(0);publicvoidincrement(){count.incrementAndGet();}7.2 ⚠️ 坑 264位变量在32位 JVM 上不是原子的// ❌ 在 32 位 JVM 上long/double 的读写不是原子的privatelongvalue0;// 32位 JVM 上可能读取到半个值// ✅ 正确使用 volatile 保证原子可见privatevolatilelongvalue0;// 64 位读写是原子的// 或者使用 AtomicLongJMM 规定对long和double的读写如果加了volatile则保证是原子操作。32位 JVM 上未加 volatile 的long可能分两次读取。7.3 ⚠️ 坑 3volatile 数组的可见性陷阱// ❌ 错误volatile 不保证数组元素的可见性privatevolatileint[]arraynewint[10];publicvoidwrite(){array[0]1;// 数组元素修改对其它线程不可见}// ✅ 正确使用 AtomicIntegerArrayprivateAtomicIntegerArrayatomicArraynewAtomicIntegerArray(10);publicvoidwrite(){atomicArray.set(0,1);// 保证可见性}// 或者使用 synchronizedprivatefinalint[]arraynewint[10];publicsynchronizedvoidwrite(){array[0]1;}注意volatile修饰数组引用时只保证引用本身的可见性不保证数组元素的可见性。7.4 ⚠️ 坑 4忘记传递性规则// ❌ 错误普通变量在 volatile 写之前/之后的可见性混淆privateinta0,b0,c0;privatevolatileintflag0;publicvoidwriter(){a1;// 普通写b2;// 普通写flag1;// volatile 写a, b 对 reader 可见}publicvoidreader(){inttempflag;// volatile 读// ✅ 一定能看到 a 1, b 2// 因为 a, b 的写 happens-before flag 的写程序次序// flag 的写 happens-before flag 的读volatile 规则// 由传递性a, b 的写 happens-before flag 的读}八、总结概念核心要点口诀JMM主内存 工作内存抽象线程私有主内存共享可见性一个线程修改其他线程可见volatile、synchronized、final原子性操作不可中断synchronized、Atomic、Lock有序性禁止重排序happens-before、volatile 屏障happens-before8 条规则定义偏序关系锁、volatile、线程、传递volatile可见性 有序性无原子性一写多读状态标志synchronized三性俱全有阻塞复合操作首选JMM 核心原则单线程内as-if-serial无需担心重排序多线程间必须通过 happens-before 保证可见性volatile状态标志 一写多读场景的首选synchronized复合操作 原子性保证DCL必须加 volatile否则可能拿到未初始化对象// 正确使用 volatile 的模板publicclassSafeVolatilePattern{privatevolatilebooleanshutdownfalse;publicvoidshutdown(){shutdowntrue;// volatile 写所有线程可见}publicvoiddoWork(){while(!shutdown){// volatile 读// 工作...}}}本文首发于 CSDN转载请注明出处。JMM 是理解 Java 并发的基础掌握 happens-before 和 volatile 是写出正确并发代码的第一步。