1. 这篇文档讲什么这篇文档专门解释 Java 内存模型JMM里面经常提到的一个概念内存屏障。你可以把内存屏障理解成JVM 或 CPU 在某些位置插入的一道“栅栏”用来限制读写操作的重排序并保证某些写入能被其他线程看到。它不是 Java 代码里直接写出来的东西大多数时候是 JVM 根据volatile、synchronized、Lock、Atomic等语义在底层生成的约束。2. 为什么需要内存屏障现代程序为了性能会有很多优化1. 编译器会重排序代码 2. JIT 会优化变量读写 3. CPU 会乱序执行 4. CPU 会使用寄存器、L1、L2、L3 缓存 5. CPU 有写缓冲 Store Buffer这些优化在单线程里通常没问题因为单线程只要求最终结果正确。但多线程就不一样了。例如classDemo{intdata0;booleanreadyfalse;voidwriter(){data100;readytrue;}voidreader(){if(ready){System.out.println(data);}}}你希望如果reader()看到readytrue那么它也应该看到data100但是在没有同步手段时编译器和 CPU 可能重排序也可能让另一个线程看不到最新写入。所以需要内存屏障来约束这些行为。3. 内存屏障到底是什么内存屏障英文一般叫Memory Barrier Memory Fence它的作用可以分成两类1. 阻止某些读写操作重排序 2. 保证某些写入对其他线程可见更通俗一点内存屏障就是告诉编译器和 CPU这条线前后的读写操作不能随便换顺序该刷新的要刷新该重新读取的要重新读取。4. 内存屏障不是 Java 对象内存屏障不是这种东西newMemoryBarrier();普通 Java 业务代码里一般不会直接写内存屏障。你写的是volatilesynchronizedLockAtomicInteger然后 JVM 根据 Java 内存模型的要求在底层插入对应的屏障或等价指令。5. 编译器屏障和 CPU 屏障内存屏障可以从两个层面理解。5.1 编译器屏障编译器屏障主要限制编译器/JIT 的优化。它告诉编译器这里前后的读写不能随便重排。比如data100;readytrue;如果ready是volatile那么编译器不能把data 100重排序到ready true后面。5.2 CPU 屏障CPU 屏障主要限制 CPU 的乱序执行和缓存行为。它可能会要求1. 前面的写操作要先对其他核心可见 2. 后面的读操作不能跑到前面去 3. 写缓冲区里的内容要按规则提交 4. 其他核心的缓存行需要失效或重新获取不同 CPU 架构的屏障指令不同。比如 x86、ARM、POWER 的内存模型强弱不同JVM 会为不同平台生成合适的指令。6. 四类常见内存屏障理论上常见有四类屏障LoadLoad StoreStore LoadStore StoreLoad其中Load 读 Store 写7. LoadLoad 屏障格式Load1 LoadLoad 屏障 Load2含义保证 Load1 的读操作先于 Load2 的读操作完成。也就是说屏障后的读不能跑到屏障前的读前面去。8. StoreStore 屏障格式Store1 StoreStore 屏障 Store2含义保证 Store1 的写操作先于 Store2 的写操作对其他线程可见。典型场景是 volatile 写之前。例如data100;readytrue;// volatile 写JVM 要保证data 100 先于 ready true 对外可见否则另一个线程看到ready true却看不到data 100就出问题了。9. LoadStore 屏障格式Load1 LoadStore 屏障 Store2含义保证 Load1 的读操作先于 Store2 的写操作。屏障后的写不能跑到屏障前的读前面去。10. StoreLoad 屏障格式Store1 StoreLoad 屏障 Load2含义保证 Store1 的写操作先于 Load2 的读操作。这是最强的一类屏障代价也通常最大。它常用于 volatile 写之后。为什么它强因为它要防止前面的写还没对其他线程可见后面的读已经开始执行11. volatile 写会插入什么屏障假设intdata0;volatilebooleanreadyfalse;voidwriter(){data100;readytrue;}对于readytrue;这是 volatile 写。JMM 语义大概可以理解成普通写 StoreStore 屏障 volatile 写 StoreLoad 屏障作用是1. volatile 写之前的普通写不能被重排序到 volatile 写之后 2. volatile 写要对其他线程可见 3. volatile 写之后的读写也不能随便跑到 volatile 写之前所以如果另一个线程看到ready true它也应该能看到之前的data 100。12. volatile 读会插入什么屏障假设voidreader(){if(ready){System.out.println(data);}}这里if(ready)是 volatile 读。JMM 语义大概可以理解成volatile 读 LoadLoad 屏障 LoadStore 屏障 后续普通读写作用是1. volatile 读之后的普通读不能被重排序到 volatile 读之前 2. volatile 读之后的普通写也不能被重排序到 volatile 读之前 3. 读到 volatile 最新值后可以看到写线程在 volatile 写之前的普通写13. volatile 的 release/acquire 语义也可以用更现代的说法理解。volatile 写release 语义data100;readytrue;// volatile writeready true有 release 语义。含义是volatile 写之前的操作不能跑到 volatile 写之后。volatile 读acquire 语义if(ready){// volatile readSystem.out.println(data);}读取ready有 acquire 语义。含义是volatile 读之后的操作不能跑到 volatile 读之前。release acquire 配合起来就能完成线程间安全发布。14. 内存屏障和 happens-before 的关系happens-before是 Java 层面的规则。内存屏障是底层实现手段之一。比如 volatile 规则对 volatile 变量的写 happens-before 后续对同一个 volatile 变量的读。Java 程序员看到的是 happens-before 规则。JVM 在底层可能通过编译器屏障 CPU 内存屏障 缓存一致性协议 特殊机器指令来实现这个规则。15. synchronized 和内存屏障synchronized也有内存语义。synchronized(lock){value10;}退出同步块相当于释放锁。释放锁前线程在同步块里的写入要对后续获取同一把锁的线程可见。synchronized(lock){System.out.println(value);}进入同步块相当于获取锁。获取锁后线程能看到之前释放同一把锁的线程所做的写入。可以理解成unlock 有类似 release 语义 lock 有类似 acquire 语义16. 内存屏障和 CPU 缓存一致性内存屏障不是直接关闭缓存。CPU 仍然会使用缓存。屏障的作用是配合缓存一致性协议让多核之间对某些内存操作达成一致。常见缓存一致性协议会保证一个核心修改了某个缓存行其他核心不能一直使用旧缓存行。内存屏障则进一步约束哪些读写必须先完成 哪些写必须对外可见 哪些读不能使用过期结果所以缓存一致性解决“多个核心缓存同一份数据怎么保持一致”内存屏障解决“读写顺序和可见性什么时候必须被强制保证”。17. 一个完整例子classSafePublish{privateintdata;privatevolatilebooleanready;publicvoidwriter(){data100;readytrue;}publicvoidreader(){if(ready){System.out.println(data);}}}执行逻辑writer 线程 1. data 100 2. volatile 写 ready true reader 线程 1. volatile 读 ready 2. 如果 ready true读取 data因为 volatile 写和 volatile 读建立 happens-beforedata 100 happens-before ready true ready true happens-before reader 读 ready reader 读 ready happens-before reader 读 data所以 reader 如果看到ready true就能看到data 100。底层靠内存屏障等机制保证这个效果。18. 双重检查锁里的内存屏障classSingleton{privatestaticvolatileSingletoninstance;publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}}returninstance;}}这里instance必须是 volatile。因为instancenewSingleton();可能被拆成1. 分配内存 2. 初始化对象 3. 把引用赋值给 instance如果没有 volatile可能重排序成1. 分配内存 2. 把引用赋值给 instance 3. 初始化对象另一个线程看到instance ! null但对象还没初始化完成。volatile会通过内存屏障禁止这种危险发布。19. 内存屏障不保证复合操作原子性即使有内存屏障下面代码也不安全volatileintcount0;count;原因count 是读、加、写三个动作 volatile 只能保证可见性和有序性 不能把三个动作变成一个不可分割的原子操作要用AtomicInteger或synchronized20. 常见误区误区 1内存屏障就是刷新主存不准确。内存屏障不是简单地“刷新主存”。它更准确的作用是限制重排序 配合缓存一致性协议保证可见性 约束读写执行顺序误区 2volatile 就是每次都绕过缓存读 RAM错误。CPU 仍然使用缓存。volatile只是要求底层用屏障和缓存一致性机制保证可见性。误区 3有内存屏障就线程安全错误。内存屏障主要解决可见性和有序性。线程安全还可能需要原子性。例如count仍然需要 Atomic 类或锁。误区 4所有 CPU 上内存屏障成本都一样错误。不同 CPU 架构的内存模型不同。一般来说x86 内存模型较强很多场景屏障成本相对低 ARM/POWER 内存模型较弱可能需要更多屏障JVM 会根据平台生成不同指令。21. 速查表概念作用LoadLoad防止后面的读跑到前面的读之前StoreStore防止后面的写跑到前面的写之前LoadStore防止后面的写跑到前面的读之前StoreLoad防止后面的读跑到前面的写之前通常最重volatile 写release 语义写之前的操作不能跑到写之后volatile 读acquire 语义读之后的操作不能跑到读之前synchronized unlock类似 releasesynchronized lock类似 acquire22. 一句话总结内存屏障是 JVM 和 CPU 用来实现 JMM 可见性、有序性规则的底层机制。它不是关闭缓存也不是简单刷新主存而是通过限制重排序、约束读写顺序、配合缓存一致性协议让多线程之间能按 Java 规则正确看到彼此的写入。