深入拆解 Java 内存模型:从原子性、可见性到有序性,彻底搞懂 happen-before 规则
深入拆解Java内存模型从原子性、可见性到有序性彻底搞懂happen-before规则在Java并发编程中Java内存模型JMM是最核心的概念之一。它不仅定义了线程与主内存之间的抽象关系还为解决并发场景下的原子性、可见性、有序性问题提供了规范保障。理解JMM是写出正确、高效并发代码的基础。JMM将内存分为主内存Main Memory和工作内存Working Memory。主内存是所有线程共享的存储了实例对象、静态变量等数据而每个线程都有自己私有的工作内存存储了该线程使用的变量的主内存副本。线程对变量的所有操作读取、赋值都必须在工作内存中进行不能直接读写主内存。不同线程之间也无法直接访问对方工作内存中的变量线程间变量值的传递需要通过主内存来完成。原子性不可分割的操作原子性是指一个操作是不可分割的要么全部执行成功要么全部不执行执行过程中不会被其他线程打断。在Java中对基本数据类型的读取和赋值操作通常是原子性的但像count这样的复合操作读取-修改-写入就不具备原子性。示例原子性问题package com.jam.demo; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; /** * 原子性示例 * * author ken */ Slf4j public class AtomicityDemo { private static int count 0; public static void main(String[] args) throws InterruptedException { int threadCount 1000; CountDownLatch countDownLatch new CountDownLatch(threadCount); for (int i 0; i threadCount; i) { new Thread(() - { try { for (int j 0; j 1000; j) { count; } } finally { countDownLatch.countDown(); } }).start(); } countDownLatch.await(); log.info(count: {}, count); } }这段代码启动1000个线程每个线程对count进行1000次自增操作预期结果是1000000但实际运行结果往往小于这个值因为count是复合操作不具备原子性。保证原子性的方式synchronized关键字通过管程Monitor机制保证同一时间只有一个线程能执行临界区代码。package com.jam.demo; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; /** * synchronized保证原子性示例 * * author ken */ Slf4j public class SynchronizedAtomicityDemo { private static int count 0; private static synchronized void increment() { count; } public static void main(String[] args) throws InterruptedException { int threadCount 1000; CountDownLatch countDownLatch new CountDownLatch(threadCount); for (int i 0; i threadCount; i) { new Thread(() - { try { for (int j 0; j 1000; j) { increment(); } } finally { countDownLatch.countDown(); } }).start(); } countDownLatch.await(); log.info(count: {}, count); } }Lock接口与synchronized类似提供了更灵活的锁机制。原子类java.util.concurrent.atomic基于CASCompare-And-Swap操作实现无锁原子性。package com.jam.demo; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; /** * AtomicInteger保证原子性示例 * * author ken */ Slf4j public class AtomicIntegerDemo { private static AtomicInteger count new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { int threadCount 1000; CountDownLatch countDownLatch new CountDownLatch(threadCount); for (int i 0; i threadCount; i) { new Thread(() - { try { for (int j 0; j 1000; j) { count.incrementAndGet(); } } finally { countDownLatch.countDown(); } }).start(); } countDownLatch.await(); log.info(count: {}, count.get()); } }可见性线程间的变量同步可见性是指当一个线程修改了共享变量的值其他线程能立即看到这个修改。CPU缓存模型与可见性问题现代CPU通常有多级缓存L1、L2、L3每个核心有自己的L1、L2缓存多个核心共享L3缓存。当线程修改变量时会先将变量从主内存加载到工作内存对应CPU缓存修改后写回主内存但写回时机不确定导致其他线程可能看不到最新值。示例可见性问题package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 可见性问题示例 * * author ken */ Slf4j public class VisibilityDemo { private static boolean flag false; public static void main(String[] args) { new Thread(() - { while (!flag) { // 空循环 } log.info(线程A检测到flag变为true结束循环); }, 线程A).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } flag true; log.info(主线程将flag设置为true); } }这段代码中主线程将flag设置为true后线程A可能永远不会结束循环因为线程A的工作内存中flag还是旧值。保证可见性的方式volatile关键字通过内存屏障Memory Barrier保证变量的可见性。当写一个volatile变量时JMM会把该线程工作内存中的变量值立即刷新回主内存当读一个volatile变量时JMM会把该线程工作内存中的变量置为无效重新从主内存读取。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * volatile保证可见性示例 * * author ken */ Slf4j public class VolatileVisibilityDemo { private static volatile boolean flag false; public static void main(String[] args) { new Thread(() - { while (!flag) { // 空循环 } log.info(线程A检测到flag变为true结束循环); }, 线程A).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } flag true; log.info(主线程将flag设置为true); } }synchronized关键字在释放锁前会将工作内存中的变量刷新回主内存在获取锁后会从主内存重新读取变量。final关键字final修饰的字段在初始化完成后其他线程能看到其正确值前提是对象没有逸出。有序性禁止指令重排序有序性是指程序执行的顺序按照代码的先后顺序执行。但在并发场景下编译器和CPU可能会对指令进行重排序以提高性能这可能导致程序执行结果与预期不符。指令重排序编译器重排序编译器在不改变单线程程序语义的前提下调整指令的执行顺序。CPU重排序CPU在执行指令时可能会调整指令的执行顺序以充分利用CPU流水线。as-if-serial语义as-if-serial语义保证不管怎么重排序单线程程序的执行结果不能被改变。编译器、runtime和CPU都必须遵守as-if-serial语义。示例有序性问题双重检查锁定package com.jam.demo; /** * 双重检查锁定示例有序性问题 * * author ken */ public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }这段代码看似没问题但instance new Singleton()这行代码可能会被重排序分配内存空间初始化对象将instance指向分配的内存地址重排序后可能变成分配内存空间将instance指向分配的内存地址初始化对象如果线程A执行到步骤2此时instance不为null但还没初始化对象线程B在第一次检查时发现instance不为null直接返回就会拿到一个未初始化的对象。保证有序性的方式volatile关键字通过内存屏障禁止指令重排序。对于volatile变量的写操作会在写操作前插入StoreStore屏障写操作后插入StoreLoad屏障对于volatile变量的读操作会在读操作前插入LoadLoad屏障读操作后插入LoadStore屏障。 修改后的双重检查锁定package com.jam.demo; /** * 双重检查锁定示例volatile保证有序性 * * author ken */ public class VolatileSingleton { private static volatile VolatileSingleton instance; private VolatileSingleton() { } public static VolatileSingleton getInstance() { if (instance null) { synchronized (VolatileSingleton.class) { if (instance null) { instance new VolatileSingleton(); } } } return instance; } }synchronized关键字保证同一时间只有一个线程执行临界区代码相当于让临界区代码串行执行自然保证了有序性。happen-before规则JMM定义的一套偏序关系通过这些规则可以判断两个操作是否有序。happen-before规则JMM的核心偏序关系happen-before规则是JMM定义的一套偏序关系用于判断两个操作之间是否存在可见性保证。如果操作A happen-before 操作B那么A的执行结果对B可见且A的执行顺序排在B之前。1. 程序次序规则在一个线程内按照代码顺序前面的操作happen-before于后面的操作。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 程序次序规则示例 * * author ken */ Slf4j public class ProgramOrderRuleDemo { public static void main(String[] args) { int a 1; int b 2; int c a b; log.info(c: {}, c); } }在主线程中int a 1happen-beforeint b 2int b 2happen-beforeint c a b所以a和b的赋值对c的计算可见。2. 管程锁定规则一个unlock操作happen-before于后面对同一个锁的lock操作。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 管程锁定规则示例 * * author ken */ Slf4j public class MonitorLockRuleDemo { private static int count 0; public static void main(String[] args) { new Thread(() - { synchronized (MonitorLockRuleDemo.class) { count 10; } }, 线程A).start(); new Thread(() - { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } synchronized (MonitorLockRuleDemo.class) { log.info(count: {}, count); } }, 线程B).start(); } }线程A先释放锁线程B后获取锁所以线程A对count的修改对线程B可见。3. volatile变量规则对一个volatile变量的写操作happen-before于后面对这个变量的读操作。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * volatile变量规则示例 * * author ken */ Slf4j public class VolatileVariableRuleDemo { private static volatile int value 0; private static boolean flag false; public static void main(String[] args) { new Thread(() - { value 10; flag true; }, 线程A).start(); new Thread(() - { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } if (flag) { log.info(value: {}, value); } }, 线程B).start(); } }这里flag是volatile变量线程A对flag的写操作happen-before线程B对flag的读操作根据传递性线程A对value的修改对线程B可见。4. 线程启动规则Thread对象的start()方法happen-before于此线程的每一个动作。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 线程启动规则示例 * * author ken */ Slf4j public class ThreadStartRuleDemo { private static int value 0; public static void main(String[] args) { value 10; Thread thread new Thread(() - { log.info(value: {}, value); }, 线程A); thread.start(); } }主线程对value的修改happen-before线程A的start()方法线程A的start()方法happen-before线程A的所有动作所以主线程对value的修改对线程A可见。5. 线程终止规则线程中的所有操作都happen-before于对此线程的终止检测。package com.jam.demo; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; /** * 线程终止规则示例 * * author ken */ Slf4j public class ThreadTerminationRuleDemo { private static int value 0; public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch new CountDownLatch(1); Thread thread new Thread(() - { try { value 10; } finally { countDownLatch.countDown(); } }, 线程A); thread.start(); countDownLatch.await(); log.info(value: {}, value); } }线程A的所有操作happen-before主线程对线程A的终止检测通过CountDownLatch.await()所以线程A对value的修改对主线程可见。6. 线程中断规则对线程interrupt()方法的调用happen-before于被中断线程的代码检测到中断事件的发生。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 线程中断规则示例 * * author ken */ Slf4j public class ThreadInterruptRuleDemo { public static void main(String[] args) { Thread thread new Thread(() - { while (!Thread.currentThread().isInterrupted()) { // 空循环 } log.info(线程A检测到中断结束循环); }, 线程A); thread.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } thread.interrupt(); } }主线程对thread的interrupt()调用happen-before线程A检测到中断事件所以线程A能正确响应中断。7. 对象终结规则一个对象的初始化完成happen-before于它的finalize()方法的开始。package com.jam.demo; import lombok.extern.slf4j.Slf4j; /** * 对象终结规则示例 * * author ken */ Slf4j public class ObjectFinalizeRuleDemo { private int value; public ObjectFinalizeRuleDemo() { this.value 10; } Override protected void finalize() throws Throwable { super.finalize(); log.info(value: {}, value); } public static void main(String[] args) { new ObjectFinalizeRuleDemo(); System.gc(); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } } }对象的初始化完成happen-beforefinalize()方法的开始所以finalize()方法中能看到value的正确值。8. 传递性如果A happen-before BB happen-before C那么A happen-before C。 在volatile变量规则的示例中线程A对value的修改Ahappen-before线程A对flag的写B线程A对flag的写Bhappen-before线程B对flag的读C所以线程A对value的修改Ahappen-before线程B对value的读C。总结JMM通过主内存与工作内存的抽象结构定义了原子性、可见性、有序性的规范并通过happen-before规则为并发编程提供了可见性保证。理解JMM的核心概念和规则是写出正确、高效并发代码的关键。在实际开发中我们可以通过synchronized、volatile、Lock、原子类等工具结合happen-before规则来解决并发场景下的各种问题。