【并发编程】Java 内存模型(JMM)深度解析

发布于:2025-07-05 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、Java内存模型

Java 内存模型(Java Memory Model,简称 JMM)是 Java 并发编程的基石之一,是一种抽象规范并不是物理意义上的东西。它定义了:在多线程环境下,变量的读写在 JVM 中是如何进行的,以及各个线程之间如何保证可见性、有序性和原子性

在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
.

1.1 为什么需要 Java 内存模型(JMM)?

在单线程程序中,代码按顺序执行,一切都很自然。但在 多线程程序中:

  • 线程之间有自己的工作内存(缓存)。
  • 如果不加控制,一个线程对变量的修改,另一个线程可能永远看不到(可见性问题)。
  • 编译器/CPU 可能会进行 指令重排序,导致语义上的先后顺序被打乱(有序性问题)。

JMM 的作用就是屏蔽不同硬件和操作系统内存访问差异的细节,让 Java 程序在不同平台下都有一致的并发行为。

.

1.2 JMM把内存模型抽象为两部分:主内存和工作内存

  • 主内存(Main Memory)
    抽象层面是所有线程共享的内存区域,存储共享变量(实例字段、静态字段等);物理层面对应物理内存(RAM) 或 JVM 堆内存。主内存是线程间数据交互的 “公共区域”,但直接读写主内存效率低(物理内存访问延迟约 100ns,远高于 CPU 缓存的 1-10ns)
  • 工作内存(Working Memory)
    抽象层面是线程私有区域,存储主内存变量的副本;物理层面对应CPU 缓存(L1/L2/L3) 和寄存器。线程对变量的所有操作(读、写)必须在工作内存中完成,再通过特定机制同步到主内存,这是为了利用 CPU 缓存提升效率,但也引入了一致性问题。
  • 线程操作变量的过程
    • 主内存 <—> 工作内存 <—> 线程执行

在这里插入图片描述

注意:JMM ≠ 物理内存。JMM 是抽象模型,工作内存对应 CPU 缓存和寄存器,主内存对应物理 RAM。

.

1.3 内存交互的 8 种原子操作:从抽象到物理

JMM 定义的 8 种操作(lock/unlock/read/load/use/assign/store/write)并非直接对应 CPU 指令,而是对线程与内存交互的抽象描述,其物理映射如下:

在这里插入图片描述

关键特性:单个操作原子,但复合操作(如 i++ = read+load+use+assign+store+write)不保证原子性,需通过 synchronized 或原子类实现。

.

1.4 happens-before原则的形式化语义与实践

happens-before 是 JMM 定义的 “逻辑时钟”,用于判断多线程操作的可见性与有序性,其核心是传递性与偏序关系

1.4.1 核心规则的底层映射

  • 程序顺序规则:单线程内,前面的操作 happens-before 后面的操作。
    底层依赖编译器的 “as-if-serial” 语义:允许重排序,但必须保证单线程执行结果不变(通过数据依赖分析避免破坏逻辑)。
  • 监视器锁规则:解锁 happens-before 后续加锁。
    底层对应 synchronized 的monitorenter(加锁)和monitorexit(解锁)指令:解锁时插入 StoreStore 屏障刷新数据到主内存,加锁时插入 LoadLoad 屏障从主内存加载最新值。
  • volatile 规则:写操作 happens-before 后续读操作。
    底层通过内存屏障实现:volatile 写后插入 StoreLoad 屏障,读前插入 LoadLoad 屏障,确保写操作的全局可见性与读操作的顺序性。
  • 线程启动 / 终止规则start() happens-before 线程内操作,线程内操作 happens-before join()返回。
    底层依赖 JVM 对线程状态的同步:start()会刷新主线程的共享变量到主内存,join()会等待线程终止并加载其修改的变量。

1.4.2 传递性的典型应用

通过传递性可构建复杂的有序关系,例如

// 线程A
volatile int flag = 0;
int data = 0;

// 步骤1:A修改data
data = 100;  
// 步骤2:A修改flag(volatile写)
flag = 1;  

// 线程B
// 步骤3:B读取flag(volatile读)
if (flag == 1) {  
    // 步骤4:B读取data
    System.out.println(data);  // 必然输出100
}
  • 步骤 1 happens-before 步骤 2(程序顺序规则);
  • 步骤 2 happens-before 步骤 3(volatile 规则);
  • 步骤 3 happens-before 步骤 4(程序顺序规则);
  • 根据传递性:步骤 1 happens-before 步骤 4,故 data 的修改对 B 可见。

.

1.5 JMM在JVM层的具体实现是怎样的 - 内存屏障

JVM 本身并不直接实现 JMM,而是通过 插入特定的内存屏障memory barrier指令,要求 CPU 遵守内存访问的某种顺序,达到 JMM 所定义的“可见性”和“有序性”。
Memory Barrier 是一种 CPU 指令,通常用于刷新本地缓存(保证主内存可见性)以及 控制指令重排序

.

1.5.1 常见的屏障指令

  • LoadLoad Barrier:禁止读操作重排序,确保前面的加载操作先于后面的加载操作执行
    • 如:Load1;LoadLoad;Load2。确保Load1数据的装载先于Load2及所有后续装载指令的装载
  • StoreStore Barrier:禁止写操作重排序,确保前面的写操作对主内存可见后,后续的写才能执行
    • 如:Store1;StoreStore;Store2。确保Store1数据对其他线程可见(刷新到内存)先于Store2及所有后续存储指令的存储
  • LoadStore Barrier:禁止读操作与写操作重排序,确保前面的读完成后才能写
    • 如:Load1;LoadStore;Store2。确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存
  • StoreLoad Barrier:最强屏障,禁止所有重排序,刷新缓存,开销最大。确保前面的写对其他线程可见后,后续读才开始执行(一般用于 volatile 写之后和读之前)
    • 如:Store;StoreLoad;Load2。确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers 会是该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令

.

1.5.2 不同屏障的性能损耗

在这里插入图片描述

示例:一个 volatile 写操作在 x86 上的开销约为普通写的 10 倍(因 StoreLoad 屏障耗时),在 ARM 上更高(需额外指令保证顺序)

.

1.5.3 不同架构下的实现

1)JVM 的内存屏障最终需映射到具体硬件架构:

  • x86 架构
    通过lock前缀指令实现,如lock addl $0,0(%%esp),强制刷新缓存并锁定总线。
    • StoreLoad 屏障:通过mfence指令实现。
    • 其他屏障:在 x86 中,部分屏障可通过内存访问顺序的自然约束隐式实现(如 LoadLoad 屏障)。
  • ARM 架构:使用dmb(数据内存屏障)和dsb(数据同步屏障)指令。

2)不同架构之间应用上的差异

  • x86 架构:因硬件天然保证 LoadLoadStoreStoreLoadStore 的顺序(TSO 模型),JVM 可省略这些屏障,仅需实现 StoreLoad 屏障;
  • ARM 架构:弱内存模型(允许更多重排序),JVM 需显式插入所有类型的屏障,故 volatile 操作在 ARM 上的开销是 x86 的 3 倍以上。

.

1.6 JMM 规定的三大特性

JMM 的核心目标是保证多线程环境下的原子性、可见性、有序性,其实现依赖硬件机制与软件规范的协同

1.6.1 原子性(Atomicity):从硬件锁到软件同步

原子性指操作不可分割(要么全执行,要么全不执行),比如保证 readloadassignusestorewrite 等单个操作是原子的,但复合操作(如 i++)不保证。还有通过 java.util.concurrent.atomic 包提供的原子类(如 AtomicInteger)内部实现了复合操作的原子性

  • 例如:int a = 1; 是原子的
  • a++ 并不是原子的(它其实是三步:读 -> 加 -> 写)

1)解决方法:
synchronizedLockAtomicInteger 等原子类。synchronized 和各种 Lock 可以让多线程访问串行化,保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。而各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile或者final关键字)来保证原子操作

2)底层实现原理:
那么上面的原子类各个如何实现,可以划分为两个层级:

  • 硬件级原子性
    CPU 通过总线锁(Bus Lock) 或缓存锁(Cache Lock) 保证单个内存操作的原子性。例如,x86 的lock前缀指令会锁定系统总线,阻止其他 CPU 访问该内存地址,确保操作原子性。
  • 软件级原子性
    • synchronized:通过 JVM 的监视器锁(monitor)实现,本质是将代码块包装为 lock + 操作 + unlock,利用硬件锁保证原子性;
    • 原子类(如 AtomicInteger):基于CAS(Compare-And-Swap) 操作,映射到硬件的cmpxchg指令(x86)或ldaxr/stlxr(ARM),通过循环重试实现复合操作的原子性;
    • Lock 接口:如 ReentrantLock,底层通过 AQSAbstractQueuedSynchronizer)的 CAS 操作 + volatile 变量实现原子性。

.

1.6.2 可见性(Visibility):从缓存一致性到内存刷新

一个线程修改了变量的值,其他线程能马上看到修改后的值。问题的根源主要是:线程对变量的修改先在工作内存中进行,未及时刷新到主内存,导致其他线程无法立即看到最新值

// 线程1
flag = true;

// 线程2
while(!flag) {
    // 可能一直等不到
}

1)解决方法:

  • volatile 关键字:确保变量的修改立即刷新到主内存,并使其他线程的工作内存中该变量副本失效。
  • synchronized 块:解锁前必须将变量刷新到主内存。
  • final 关键字:初始化后不可变,保证可见性。
  • Lock,例如 ReentrantLocklock/unlock

2)底层实现原理:
可见性指一个线程修改的变量对其他线程即时可见,其实现依赖缓存刷新失效机制

  • volatile 的可见性原理
    • 写操作:触发StoreStore 屏障(强制缓存数据刷新到主内存)+StoreLoad 屏障(确保刷新完成),通过 MESI 协议的 “修改 - 失效” 机制,使其他线程的缓存副本失效;
    • 读操作:触发LoadLoad 屏障(清空本地缓存失效队列)+LoadStore 屏障(强制从主内存读取最新值)。
  • synchronized 的可见性原理
    解锁(unlock)时,JVM 会插入StoreStore 屏障,强制将工作内存中的变量刷新到主内存;加锁(lock)时,插入LoadLoad 屏障,强制从主内存加载最新值,确保线程进入同步块时能看到其他线程的修改。
  • final 的可见性原理
    final 变量初始化后不可修改,JVM 在构造函数结束时插入StoreStore 屏障,确保 final 变量的值在对象引用可见前已完全初始化,其他线程不会看到 “半初始化” 的 final 变量。

.

1.6.3 有序性(Ordering):从指令重排序到内存屏障

Java 程序中的代码 不一定会按照编写的顺序执行。编译器或 CPU 为了优化性能可能会做 指令重排序。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。多线程下,为了避免破坏语义,需要通过happens-before规则来确保“看起来是有序的”。

1)底层实现原理:
有序性指程序执行顺序符合预期,核心是解决编译器 / CPU 指令重排序导致的多线程语义混乱:

  • 重排序的根源
    • 编译器重排序:Java 编译器(如 javac)为优化性能调整指令顺序(如将无关的读操作提前);
    • CPU 重排序:处理器通过乱序执行(Out-of-Order Execution) 提升效率(如先执行无依赖的指令)。
  • 有序性保证机制
    • happens-before 规则:通过定义操作间的偏序关系,确保多线程下的 “逻辑有序性”(即使物理上重排序,也需满足规则);
    • 内存屏障:JVM 通过插入特定屏障指令阻止重排序,映射到硬件指令:
      • x86 架构:通过mfence(StoreLoad)、lfence(LoadLoad)、sfence(StoreStore)实现;
      • ARM 架构:通过dmb(数据内存屏障)、dsb(数据同步屏障)实现。

.

二、Java内存模型底层实现 - 案例分析

Java 内存模型(JMM)在 JVM 层的实现涉及字节码生成内存屏障插入对象头管理垃圾回收 等多个层面。
在 Java 并发编程中,volatilesynchronizedfinal等关键字以及显式锁(如ReentrantLock)的内存语义,均通过 JVM 插入的内存屏障(Memory Barrier)来实现 Happens-Before 原则。下面我们对其核心实现机制进行详细解析

2.1 volatile 关键字的实现

  • 字节码层面:对 volatile 变量的读写会生成特殊的字节码指令(如 getfield/putfield),并在运行时通过内存屏障保证可见性。
  • HotSpot JVM 实现:在 volatile 写操作后插入 StoreLoad 屏障,在读操作前插入 LoadLoad 屏障。例如:
// Happens-Before 关系:1 → 2 → 3 → 4(通过 volatile 的内存屏障和传递性规则)
public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;          // 1. 普通写
        flag = true;    // 2. volatile写
        // 对应字节码:putfield,JVM会在此插入 StoreStore屏障 → StoreLoad屏障
    }

    public void reader() {
        if (flag) {     // 3. volatile读
            // 对应字节码:getfield,JVM会在此插入 LoadLoad屏障 → LoadStore屏障
            int i = a;  // 4. 普通读(一定看到a=1)
        }
    }
}

.

2.2 synchronized 关键字的实现

  • 字节码层面:synchronized 同步代码块通过 monitorentermonitorexit 指令实现。synchronized 同步方法通过方法表中的 ACC_SYNCHRONIZED 标志位标识。
    • 锁获取(monitorenter):在获取锁后插入LoadLoad 屏障和LoadStore 屏障:确保后续操作读取到最新值
    • 锁释放(monitorexit):在释放锁前插入StoreStore 屏障和StoreLoad 屏障:确保锁内的所有操作结果刷新到主内存
  • HotSpot JVM 实现
    • 偏向锁:对象头(Mark Word)记录第一个获取锁的线程 ID,无竞争时无需 CAS 操作。
    • 轻量级锁:通过 CASMark Word 替换为锁记录指针,失败则膨胀为重量级锁。
    • 重量级锁:依赖操作系统的互斥量(Mutex),线程挂起和唤醒开销大。
// Happens-Before 关系:1(锁释放前) → 2(锁获取后)(通过管程锁定规则)
public class SyncExample {
    int a = 0;

    public synchronized void writer() {  // 锁获取
        a = 1;  // 1. 写操作
    }  // 锁释放(隐式插入:StoreStore屏障 → StoreLoad屏障)

    public synchronized void reader() {  // 锁获取(隐式插入:LoadLoad屏障 → LoadStore屏障)
        int i = a;  // 2. 读操作(一定看到a=1)
    }
}

.

2.3 final 关键字的内存屏障

对象构造函数中的final字段赋值:在构造函数结束前插入StoreStore 屏障:确保 final 字段的写操作先于对象引用对其他线程可见

// Happens-Before 关系:1 → 对象引用对其他线程可见(通过 final 字段的内存语义)
public class FinalExample {
    final int x;
    int y;

    public FinalExample() {
        x = 3;    // 1. final字段赋值
        y = 4;    // 2. 普通字段赋值
        // 隐式插入:StoreStore屏障(确保x的写操作完成)
    }
}

.

2.4 Java 原子类底层实现

Java 原子类(如AtomicIntegerAtomicReference等)是java.util.concurrent.atomic包下的核心组件,其设计直接遵循 JMM(Java 内存模型)的规范,通过硬件原语、volatile 变量和内存屏障的协同,保证多线程环境下的原子性、可见性和有序性。以下从 JMM 三大特性的角度,深度解析原子类的底层实现机制:

2.4.1 原子性保证:基于硬件 CAS 指令与 Unsafe 类

JMM 要求原子操作 “不可分割”,原子类的原子性实现依赖CASCompare-And-Swap)操作,而 CAS 的底层是硬件级别的原子指令,直接映射到 JMM 对 “原子性” 的定义。

1)CAS 操作的硬件基础
CAS 是一种无锁同步机制,其核心逻辑为:

// CAS伪代码:比较并交换
boolean compareAndSwap(int[] arr, int index, int expect, int update) {
    if (arr[index] == expect) {  // 比较当前值与预期值
        arr[index] = update;     // 若相等则更新
        return true;
    }
    return false;
}

该操作通过硬件指令实现原子性:

  • x86 架构:映射为cmpxchg指令(带lock前缀时,通过总线锁或缓存锁保证原子性);
  • ARM 架构:映射为ldaxr(加载并标记)和stlxr(存储并检查标记)指令对,通过 LL/SCLoad-Linked/Store-Conditional)机制保证原子性。
    硬件指令的原子性直接满足 JMM 对 “单个操作不可分割” 的要求,是原子类实现原子性的基石。

2) 原子类对 CAS 的封装:Unsafe 类的作用
Java 原子类通过sun.misc.Unsafe类(底层 JVM 接口)调用 CAS 指令,例如AtomicInteger的核心实现:

public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;  // volatile修饰的共享变量

    // 原子更新操作:调用Unsafe的CAS方法
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    // 自增操作:循环CAS实现
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}
  • unsafe.compareAndSwapInt是 native 方法,直接调用底层cmpxchg(x86)或ldaxr/stlxr(ARM)指令,保证操作原子性;
  • 对于复合操作(如incrementAndGet),通过循环 CAS(自旋重试)实现:若一次 CAS 失败(被其他线程修改),则重新读取最新值并再次尝试,直至成功,从而将 “读 - 改 - 写” 复合操作转化为一系列原子 CAS 操作,符合 JMM 对原子性的扩展要求。

.

2.4.2 可见性保证:volatile 变量与缓存刷新

JMM 要求 “一个线程的修改对其他线程可见”,原子类通过volatile 修饰共享变量+CAS 操作的缓存刷新机制满足这一要求。

1)共享变量的 volatile 修饰
所有原子类的核心变量均用volatile修饰,例如:

  • AtomicIntegervalue:private volatile int value;
  • AtomicReferencevalue:private volatile V value;

volatile的可见性原理(符合 JMM 规范):

  • 写操作:通过StoreStore屏障强制将修改刷新到主内存,并通过 MESI 协议使其他线程的缓存副本失效;
  • 读操作:通过LoadLoad屏障强制从主内存读取最新值,避免使用本地缓存的旧值。

这确保原子类的变量修改能被其他线程即时感知,符合 JMM 的可见性标准。

.

2)CAS 操作的缓存同步机制
CAS 操作不仅是原子的,还会触发缓存刷新:

  • 当 CAS 成功修改变量时,硬件会自动执行 “缓存写回(Write-Back)” 操作,将修改同步到主内存;
  • 其他线程的 CAS 操作会读取主内存的最新值(因旧缓存副本已被 MESI 协议标记为无效),从而保证可见性。

例如,线程 A 通过 CAS 修改AtomicIntegervalue后,线程 B 的下一次 CAS 会读取主内存的新值,而非本地缓存的旧值,这与 JMM “工作内存→主内存→其他工作内存” 的同步流程完全一致

.

2.4.3 有序性保证:内存屏障与 CAS 的天然有序性

JMM 通过 happens-before 规则和内存屏障保证有序性,原子类通过 CAS 操作的天然有序性 + volatile 的内存屏障满足这一要求。

.
1)CAS 操作的天然有序性

硬件级 CAS 指令具有全序性(Total Order):在多核 CPU 中,所有 CAS 操作的执行顺序在全局可见,且任一 CAS 操作的结果对后续操作可见。这种硬件特性天然符合 JMM 的 “有序性” 要求 —— 即使编译器或 CPU 进行指令重排序,CAS 操作的相对顺序也不会被打破。

例如,线程 A 的CAS(value, 1, 2)操作必然 happens-before 线程 B 的 CAS(value, 2, 3) 操作,因为 B 的 CAS 依赖 A 的结果,硬件会保证这种顺序。

.

2)volatile 的内存屏障增强有序性

原子类中 volatile 变量的读写会插入内存屏障,进一步阻止指令重排序:

  • 写操作(如set方法):JVM 会在volatile写后插入StoreLoad屏障,确保写操作完成后再执行后续读操作;
  • 读操作(如get方法):JVM 会在volatile读前插入LoadLoad屏障,确保读操作前的所有读操作已完成。

AtomicInteger的set方法为例:

public final void set(int newValue) {
    value = newValue;  // volatile写,触发StoreStore+StoreLoad屏障
}

这保证了set操作的结果对后续所有操作可见,符合 JMM 的volatile变量规则(写操作 happens-before 后续读操作)。

.

2.4.4 原子类与 JMM 内存交互操作的对应

JMM 定义的 8 种内存交互操作(lock/unlock/read/load/use/assign/store/write),在原子类中均有明确映射:
在这里插入图片描述

例如,AtomicInteger.incrementAndGet()的流程完全遵循 JMM 交互规则:

  1. read:从主内存读取value到工作内存;
  2. load:载入寄存器,计算value+1
  3. use:将value作为预期值,value+1作为更新值,执行 CAS;
  4. 若 CAS 成功:assign(工作内存变量设为value+1)→storewrite(同步到主内存);
  5. 若 CAS 失败:重复 1-4,直至成功。

.

2.4.5 特殊原子类的扩展实现(以 LongAdder 为例)

对于高并发场景,LongAdder等原子类通过 “分段锁” 思想优化性能,但仍严格遵循 JMM:

  • 分段设计:将全局变量拆分为多个Cell@Contended修饰,避免伪共享),每个Cellvolatile long
  • 原子性:对单个Cell的修改通过 CAS 实现,全局结果通过累加所有Cell的值获得;
  • 可见性Cellvolatile修饰保证单个分段的修改可见;
  • 有序性Cell的读写插入内存屏障,符合happens-before规则。

这种设计在提升性能的同时,未破坏 JMM 的任何规范,是 “高效性” 与 “规范性” 平衡的典范。

.

2.5 显式锁(如ReentrantLock)的内存屏障

ReentrantLock基于 AQS(AbstractQueuedSynchronizer)实现,其内存屏障与synchronized类似:

  • lock () 方法:在获取锁后插入内存屏障,确保后续操作读取到最新值。
  • unlock () 方法:在释放锁前插入内存屏障,确保锁内操作结果刷新到主内存。
// Happens-Before 关系:与synchronized一致,通过锁的获取和释放插入内存屏障
public class LockExample {
    private final Lock lock = new ReentrantLock();
    int a = 0;

    public void writer() {
        lock.lock();
        try {
            a = 1;  // 1. 写操作
        } finally {
            lock.unlock();  // 隐式插入:StoreStore屏障 → StoreLoad屏障
        }
    }

    public void reader() {
        lock.lock();
        try {
            int i = a;  // 2. 读操作(一定看到a=1)
        } finally {
            lock.unlock();
        }
    }
}

.
.

三、JMM 的实践陷阱与优化策略

理解 JMM 的底层机制是避免并发陷阱的关键,常见问题与解决方案:

3.1 伪共享(False Sharing)

问题:多个线程修改同一缓存行中的不同变量,导致缓存行频繁失效(MESI 协议触发),性能下降 70% 以上。

// 两个变量共享同一缓存行(64字节)
int a = 0;  
int b = 0; 

// 线程1频繁修改a,线程2频繁修改b,导致缓存行反复失效

解决方案:

  • 使用@Contended注解(JDK8+):自动填充缓存行,确保变量独占 64 字节;
  • 手动填充:添加冗余字段至 64 字节(如 7 个 long 类型字段)。

.

3.2 volatile 的滥用

问题volatile 仅保证可见性和有序性,不保证原子性,滥用会导致错误。

volatile int count = 0;  
// 多线程执行count++,结果可能小于预期(非原子操作)

解决方案

  • 原子类:AtomicInteger(CAS 保证原子性);
  • 结合锁:synchronizedLock确保复合操作原子性。

.

3.3 指令重排序的隐蔽问题

问题:编译器重排序可能破坏多线程语义,即使无数据依赖。

int a = 0, b = 0;  
boolean flag = false;  

// 线程A
a = 1;         // 操作1
flag = true;   // 操作2

// 线程B
if (flag) {    // 操作3
    b = a;     // 操作4,可能b=0(操作2被重排到操作1前)
}

解决方案

  • 添加 volatile:volatile boolean flag,通过 StoreLoad 屏障阻止重排序;
  • 使用synchronized:通过锁的有序性保证操作顺序。

.
.

四、JMM 的设计本质与价值

Java 内存模型是 “硬件复杂性” 与 “软件易用性” 的折中:

  • 底层依赖 CPU 缓存总线锁MESI 协议等硬件机制;
  • 上层通过 happens-before 规则、内存屏障等软件规范,为开发者提供统一的并发抽象,屏蔽硬件差异。

掌握 JMM 不仅能避免并发陷阱,更能指导高性能并发程序的设计(如减少 volatile 使用、优化缓存行对齐、合理选择同步机制)。未来 JMM 将持续演进,以适应异构计算(GPU、NVM)等新场景,但其 “安全性与性能平衡” 的核心设计哲学将始终不变。


网站公告

今日签到

点亮在社区的每一天
去签到