从一段诡异的代码说起publicclassVolatileDemo{privatestaticbooleanflagfalse;// 不加volatilepublicstaticvoidmain(String[]args){newThread(()-{while(!flag){// 线程B死循环等待flag变成true}System.out.println(线程B退出);}).start();Thread.sleep(1000);flagtrue;// 线程A1秒后将flag设为trueSystem.out.println(线程A已设置flagtrue);}}你猜结果是什么在server模式下线程B永远不会退出——明明线程A已经把flag改成了true线程B却看不见。这不是CPU缓存的问题吗但JVM内存模型中的本地内存又是什么本文核心问题Java内存模型JMM到底是什么和JVM内存结构是一回事吗可见性问题是怎么发生的底层原因是什么volatile为什么能保证可见性内存屏障做了什么指令重排序是什么volatile如何禁止重排序happens-before规则有哪些怎么用DCL单例为什么必须加volatilevolatile能保证原子性吗和synchronized的区别JMM和硬件内存模型的关系是什么读完本文你将彻底理解Java并发编程中最基础也最核心的可见性、有序性、原子性三大问题。一、JMM到底是什么和JVM内存结构是两码事疑问JMM和JVM运行时数据区堆、栈、方法区是什么关系回答这是两个完全不同层面的概念80%的人会搞混。JVM内存结构运行时数据区——物理划分这是你在JVM系列学到的JVM运行时数据区 ├── 线程私有 │ ├── 程序计数器 │ ├── 虚拟机栈栈帧局部变量表、操作数栈 │ └── 本地方法栈 └── 线程共享 ├── 堆对象实例 └── 方法区/元空间类信息、常量关键词内存区域物理划分、对象存在哪、GC发生在哪。JMMJava内存模型——并发模型抽象JMM是一个抽象规范定义了一套规则JMM抽象模型 ├── 主内存Main Memory—— 所有线程共享存放共享变量 ├── 本地内存Local Memory—— 每个线程私有存放共享变量的副本 └── 八个原子操作lock/unlock/read/load/use/assign/store/write关键词并发访问规则、可见性保证、happens-before约束。举一个例子彻底区分publicclassUser{privateStringname张三;// name存储在堆中JVM内存结构视角}// 线程Auser.setName(李四);// JMM视角线程A在本地内存中修改副本再同步到主内存// 线程BSystem.out.println(user.getName());// JMM视角线程B从主内存读取可能是旧值JVM内存结构回答name这个字符串对象存在堆里JMM回答线程B能否看到线程A的修改、什么时候能看到一句话总结JVM内存结构管东西放哪JMM管多个线程怎么读写同一个东西。二、可见性问题的真相——从CPU缓存到JMM抽象疑问为什么线程A改了flag线程B看不见到底是CPU缓存还是JMM的锅回答三层递进关系——底层硬件CPU缓存 → JMM规范抽象 → Java代码行为。你之前写的那篇「线程本地缓存CPU缓存」已经点到了核心——“线程没有本地内存”那是什么2.1 硬件层CPU三级缓存CPU 核心0 CPU 核心1 │ │ L1 缓存32KB私有 L1 缓存32KB私有 │ │ L2 缓存256KB私有 L2 缓存256KB私有 │ │ └──────── L3 缓存 ──────────┘ 共享 │ 主内存RAM可见性问题的硬件根因每个CPU核心有自己的L1/L2高速缓存。线程A运行在核心0上修改了变量flag这个修改可能只停留在核心0的L1缓存里还没有刷新到主内存。线程B运行在核心1上从自己核心的L1缓存里读到的还是旧值。这就是你文章中线程本地内存其实是CPU缓存的正确答案——JMM规范里说的本地内存在硬件上对应的是CPU的私有缓存。2.2 JMM层抽象模型JMM把这个硬件事实抽象为主内存和本地内存JMM抽象模型 线程A 主内存 线程B ┌──────────┐ ┌──────────┐ ┌──────────┐ │ flagtrue│ │ flagfalse│ │ flagfalse│(不可见) │ (副本) │ │ (主内存) │ │ (副本) │ └──────────┘ └──────────┘ └──────────┘线程A修改了flag但还没有写回主内存线程B从主内存读到的还是falseJMM不保证一个线程的修改立即可见于其他线程除非显式使用同步机制2.3 Java代码层privatestaticbooleanflagfalse;// 没有volatile// 线程Aflag true; // 可能只写到CPU缓存没到主内存// 线程Bwhile(!flag){} // 可能只读CPU缓存看不到线程A的修改三层总结Java层: 没有volatile修饰 → JMM不保证可见性 JMM层: 允许线程在本地内存操作 → 不需要立即同步回主内存 硬件层: CPU缓存延迟刷新 → 其他核心看不到三、volatile如何保证可见性内存屏障的魔法疑问volatile到底做了什么让flag的修改能被所有线程看见回答volatile通过在指令序列中插入内存屏障Memory Barrier强制完成三件事。3.1 volatile的读写语义privatestaticvolatilebooleanflagfalse;加上volatile后JMM规定操作语义volatile写将当前线程本地内存中修改的值立即刷新到主内存volatile读每次读取都从主内存重新加载不使用本地内存的缓存值3.2 内存屏障的种类和作用JMM定义了四种内存屏障屏障类型 指令示例 作用 ────────────────────────────────────────────────── LoadLoad Load1;LoadLoad;Load2 确保Load1在Load2之前完成 StoreStore Store1;StoreStore;Store2 确保Store1在Store2之前完成 LoadStore Load1;LoadStore;Store2 确保Load1在Store2之前完成 StoreLoad Store1;StoreLoad;Load2 确保Store1在Load2之前完成最重volatile的插入规则// volatile写之前插入 StoreStore 屏障// 确保在写volatile之前之前的普通写操作全部完成storestore();volatile变量新值;// volatile写之后插入 StoreLoad 屏障// 确保本次volatile写对后续读可见storeload();// // volatile读之后插入 LoadLoad 屏障// 确保后续普通读操作能读到最新值intvalvolatile变量;loadload();// volatile读之后插入 LoadStore 屏障// 确保后续普通写操作不重排到volatile读之前loadstore();用底层术语讲内存屏障本质上是一条CPU指令如x86的mfence、lfence、sfence它强制CPU将写缓冲区的数据刷到缓存/内存并使其他核心的缓存行失效。3.3 缓存一致性协议MESIvolatile除了内存屏障还依赖CPU的缓存一致性协议MESI四种状态 M (Modified) : 该缓存行只在本核心已被修改需要写回主内存 E (Exclusive) : 该缓存行只在本核心与主内存一致 S (Shared) : 该缓存行在多个核心与主内存一致 I (Invalid) : 该缓存行无效需要从主内存重新读取 volatile写时 → 将本地缓存行状态置为M → 通过总线发送消息使其他核心的对应缓存行失效置为I → 其他核心读取时发现缓存失效从主内存重新加载可见性的完整链路线程A写volatile变量: StoreStore屏障 → 刷新写缓冲区 → CPU发RFO消息 → 其他核心缓存行失效(I) → 写入主内存 → StoreLoad屏障 线程B读volatile变量: 本地缓存失效(I) → 从主内存加载 → LoadLoad屏障 → 读到最新值四、指令重排序与volatile的有序性保证疑问加了volatile就能禁止指令重排吗什么是重排序回答volatile能禁止特定位置的重排序但不是禁止全部。4.1 编译器和CPU的重排序// 你写的代码a1;// 1b2;// 2flagtrue;// 3 (volatile写)c3;// 4d4;// 5JMM允许的重排序范围重排序自由区 a1 和 b2 可以互换1和2之间没有屏障 c3 和 d4 可以互换4和5之间没有屏障 重排序禁止区 所有在 volatile写之前的操作不能重排到volatile写之后 所有在 volatile写之后的操作不能重排到volatile写之前本质volatile就像一个栅栏只能管住栅栏两侧的操作不互换但栅栏同侧的操作依然可以自由重排。4.2 一个经典例子// 线程Adata100;// 1readytrue;// 2 (volatile写)// 线程Bif(ready){// 3 (volatile读)System.out.println(data);// 4}volatile保证了什么1一定在2之前执行不会被重排到2之后3一定在4之前执行不会被重排到4之后所以线程B看到readytrue时一定能看到data100如果没有volatile修饰ready1和2可能重排线程B可能看到readytrue但data0data还没赋值。五、happens-before规则——JMM的终极法则疑问除了volatile还有哪些情况能保证可见性回答JMM定义了一套happens-before规则只要满足其中一条前一个操作的结果就对后一个操作可见。八大happens-before规则1.程序次序规则同一个线程内前面的代码 happens-before 后面的代码inta1;// 1intb2;// 2// 1 happens-before 22.volatile变量规则volatile写 happens-beforevolatile读volatileintv;v1;// 写intxv;// 读能看到v13.锁规则unlock happens-before locksynchronized(obj){a1;}// 释放锁synchronized(obj){intxa;}// 获取锁x一定等于14.传递性AhbB,BhbC→AhbC// 结合规则12volatile写前的所有操作对volatile读后的所有操作可见5.线程启动规则Thread.start()happens-before 该线程的run()t.start();// t.run()能看到start()之前的所有修改6.线程终止规则线程的所有操作 happens-beforejoin()返回 t.join();// join返回后能看到t线程的所有修改7.线程中断规则interrupt()happens-before 被中断线程检测到中断 t.interrupt();// t检测到中断时能看到interrupt()之前的所有修改8.对象终结规则构造函数执行完 happens-beforefinalize()happens-before是最重要的并发概念它是判断线程B能不能看到线程A的修改的唯一标准。不满足任何一条规则就不能保证可见性。六、DCL单例为什么必须用volatile疑问双重检查锁定DCL单例模式为什么volatile不能省回答因为new操作不是原子的没有volatile会导致指令重排序线程可能拿到半初始化的对象。6.1 没有volatile的DCLpublicclassSingleton{privatestaticSingletoninstance;// 没有volatilepublicstaticSingletongetInstance(){if(instancenull){// 第一次检查synchronized(Singleton.class){if(instancenull){// 第二次检查instancenewSingleton();// 问题出在这}}}returninstance;}}6.2 new Singleton() 的实际执行过程JVM将这一行代码分解为三条指令memory allocate(); // 1. 分配内存空间 ctorInstance(memory); // 2. 调用构造函数初始化对象 instance memory; // 3. 将instance指向分配的内存地址问题指令2和3可能被重排序memory allocate(); // 1 instance memory; // 3重排后先执行 ← instance已经非null了 ctorInstance(memory); // 2重排后后执行 ← 但对象还没初始化多线程下的灾难时间线 T1: 进入synchronized执行new操作 T1: 分配内存 → instance指向内存但还没初始化 T2: 第一次检查 instance ! null → 直接返回instance T2: 拿到一个没初始化的对象可能NPE或拿到错误的字段值 T1: 初始化对象已经晚了6.3 加volatile解决privatestaticvolatileSingletoninstance;volatile禁止了指令2和3的重排序。因为instance memory是一个volatile写它之前的ctorInstance(memory)普通写不能重排到volatile写之后。完整的DCLpublicclassSingleton{privatestaticvolatileSingletoninstance;publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();// volatile保证安全}}}returninstance;}}七、volatile不能保证原子性疑问volatile保证了可见性那i用volatile安全吗回答不安全。volatile只保证可见性和有序性不保证原子性。privatestaticvolatileintcount0;// 10个线程各执行10000次 countcount0→ 最终结果 ≠100000远小于预期原因count不是原子操作它分为三步1. 从主内存读取count的当前值比如42 2. 在CPU里执行 42 1 43 3. 把43写回主内存并发问题时间线 T1: 读取 count 42 T2: 读取 count 42 ← 两个线程都读到了42 T1: count 43 ← T1写回 T2: count 43 ← T2写回也写43两次但值只加了1volatile能做到什么T1写回后T2能立即看到最新值不会读到过期值但不能阻止T2在T1写回之前就已经读取了旧值。解决方案方案用法synchronizedsynchronized(this) { count; }AtomicIntegeratomicInteger.incrementAndGet()LongAdder高并发计数最优privatestaticfinalAtomicIntegercountnewAtomicInteger(0);count.incrementAndGet();// CAS保证原子性八、JMM与硬件内存模型的对应关系疑问JMM为什么设计得这么抽象直接映射硬件不更简单吗回答因为不同CPU架构的内存模型差异巨大JMM必须屏蔽硬件差异提供统一的行为保证。硬件平台内存模型特点重排序程度x86/x64强内存模型TSO只允许StoreLoad重排ARM/PowerPC弱内存模型几乎所有重排都可能SPARCTSO或RMO可选取决于配置JMM的设计折中强一致性模型如x86 TSO: 优点程序员容易理解几乎不需要内存屏障 缺点限制了硬件优化性能差 弱一致性模型如ARM: 优点硬件可以大幅重排性能好 缺点程序员需要大量使用屏障容易出错 JMM的折中: 为Java程序员提供统一的、易理解的happens-before规则 用volatile/synchronized声明需要保证可见性的地方 JVM负责在不同平台上插入对应的内存屏障同一个volatile不同平台的实现// Java代码volatile写x1;// JVM在x86上的实现只禁止StoreLoad重排用mfencemov[x],1mfence// JVM在ARM上的实现禁止所有相关重排用dmbdmb ish str r1,[r0]dmb ishJMM的意义你只需要写一次volatile剩下的屏障插入、平台适配全由JVM负责。这就是一次编写到处运行在并发领域的体现。总结JMM是并发访问的抽象规范JVM内存结构是内存区域的物理划分两者完全不同的概念可见性问题的硬件根因是CPU私有缓存JMM用本地内存抽象了它volatile通过内存屏障强制刷新写缓冲区、使其他核心缓存失效保证可见性volatile通过禁止特定位置的重排序保证有序性但不禁止所有重排happens-before是判断并发操作间可见性的唯一标准掌握八条规则即可DCL单例必须加volatile因为new操作可能被重排导致拿到半初始化对象volatile不保证原子性i这种复合操作需要synchronized或AtomicIntegerJMM的设计目的是屏蔽不同硬件平台的差异提供统一的内存可见性保证下一篇预告第2篇——synchronized与ReentrantLock深度对比。