Java 内存模型(Java Memory Model,简称 JMM),是 Java 并发编程的核心底层规范,主要用于描述 Java 中内存对象的可见性处理逻辑。它定义了线程和主内存之间的交互规则,解决多线程环境下数据不一致、指令执行顺序混乱等问题,是理解 volatile、synchronized 等关键字底层原理的基石。
多线程交互的本质:共享内存的「数据对话」
多线程之间存在两种交互方式:
- 通过内存共享实现交互:线程间通过读写共享变量传递信息(Java 采用此方式)
- 通过交互实现内存共享:线程间通过消息传递机制间接同步数据(如 Erlang 并发模型)
Java 选择内存共享模式,意味着所有线程操作的变量均存储在主内存中,线程自身持有变量的副本(位于 CPU 缓存或寄存器)。JMM 提供了 volatile
、synchronized
、final
三个关键关键字,分别针对并发编程的三大核心问题:
- 可见性:一个线程修改变量后,其他线程能否立即看到变化(
volatile
/synchronized
/final
) - 原子性:操作是否具备「不可分割」的特性(
synchronized
/CAS) - 有序性:指令执行顺序是否与代码编写顺序一致(
volatile
/synchronized
)
而 CPU 缓存机制(数据副本不一致)与指令重排序(编译器 / 处理器优化导致顺序改变),是破坏数据可见性的两大核心技术挑战。
重排序:编译器与处理器的「性能优化双刃剑」
无论是编译器还是处理器,都会对代码进行「性能优化」,其中「指令重排序」是重要手段 —— 在不改变程序最终结果的前提下,调整指令执行顺序以提高效率。
1. 编译器重排序
- 发生阶段:编译期(javac 生成字节码时)
- 优化手段:删除冗余指令、合并计算、调整循环内不变量(如将循环内的常量计算移到循环外)
- 典型案例:
int a = 1;
int b = 2;
int c = a + b; // 编译器可能重排为 b=2 → a=1 → c=3(结果不变)
2. 处理器重排序
- 发生阶段:运行期(CPU 执行指令时)
- 优化手段:利用流水线并行执行指令、缓冲写操作(Store Buffer)异步刷入主存
- 典型影响:
若线程 A 先写变量x
再写y
,CPU 可能因缓冲写优化,先写y
再写x
,导致线程 B 读取时看到「y 已更新但 x 未更新」的中间状态。
伪共享:CPU 缓存行的「连带失效陷阱」
CPU 缓存以缓存行(Cache Line,通常 64 字节)为单位存储数据,缓存一致性协议(如 MESI)保证缓存与主存的数据同步,但一次失效操作会针对整个缓存行。
问题场景
假设变量 A
(8 字节)与 B
(8 字节)存储在同一缓存行:
- 线程 1 修改
A
,触发缓存行失效,线程 2 访问B
时需从主存重新加载(即使B
未被修改) B
的缓存因「非自身原因」频繁失效,导致性能下降,即「伪共享」(False Sharing)。
解决方案
- 空间换时间:让变量独占缓存行,避免其他变量与其共存。
- Java 实现:
Java 1.8 引入@sun.misc.Contended
注解,在变量或类级别添加填充字节:
@sun.misc.Contended // 需加 JVM 参数 -XX:-RestrictContended 禁用限制
class Counter {
private volatile long value = 0; // 自动填充至 64 字节,独占缓存行
}
- 该注解会在目标变量前后添加填充数据,确保其单独占据一个缓存行,消除伪共享影响。
汇编指令 lock
:CPU 硬件级的同步基石
lock
是 Intel CPU 提供的硬件级指令,用于解决多 CPU 环境下的缓存一致性问题,是 volatile 和 CAS 的底层实现基础。
核心功能
- 锁定范围进化:
- Pentium 及之前:锁定系统总线,阻止其他 CPU 访问内存(性能开销大)
- 新架构(如 Core):锁定目标缓存行(Cache Line Locking),通过 MESI 协议广播变更,仅影响当前缓存行(高效)
- 指令屏障作用:禁止 CPU 对
lock
指令前后的操作进行重排序(保障有序性) - 强制数据同步:将当前 CPU 缓存的数据刷入主存,并使其他 CPU 对应缓存行失效(保障可见性)
典型应用
lock
指令是 volatile 写操作的底层实现 —— 当线程写入 volatile 变量时,JIT 会生成 lock
前缀的汇编指令,确保数据立即同步到主存并通知其他线程。
汇编指令 cmpxchg
:AQS 底层的「原子比较魔法」
cmpxchg
(Compare and Exchange)是 CPU 提供的原子比较交换指令,Java 中的 Unsafe.compareAndSwapXXX
方法(如 compareAndSwapInt
)依赖该指令实现。
指令逻辑
cmpxchg target, expected_value, new_value
; 若 target == expected_value,则设置 target = new_value,否则不改变
多 CPU 环境下,cmpxchg
会添加 lock
前缀,确保操作原子性:
lock cmpxchg [address], eax ; 锁定缓存行,保证比较-交换操作不可分割
在 AQS 中的作用
Java 并发包的核心框架 AQS(AbstractQueuedSynchronizer),通过 cmpxchg
实现自旋锁:
- 获取锁时,用 CAS 尝试修改状态(如
state
变量) - 释放锁时,通过 CAS 唤醒等待线程
该机制确保 AQS 在获取 / 释放锁时具备内存可见性、原子性和有序性,是ReentrantLock
、Semaphore
等工具的底层支撑。
原子性操作 CAS:无锁编程的「高效与风险」
CAS(Compare-And-Swap,比较并交换)是实现无锁并发的核心机制,通过 CPU 自旋实现原子操作,无需操作系统介入。
核心流程
- 读取当前值(V):获取共享变量的当前值
- 比较期望值(A):判断当前值是否等于预期值
- 交换新值(B):若相等则更新为新值,否则重试(自旋)
基于缓存锁定的原子性
CAS 通过「缓存锁定」(Cache Locking)保证原子性:
- 写入数据后,通过 MESI 协议使其他 CPU 缓存的该变量失效
- 后续读取时强制从主存加载最新值,确保数据一致性
三大核心问题
- ABA 问题:
- 场景:变量从 A→B→A,CAS 误认为未修改(如链表节点删除后重建)
- 解决:引入版本号,使用
AtomicStampedReference
(记录值 + 时间戳)
- 单一变量限制:
- 局限:仅支持单个变量原子操作,多变量需封装为对象
- 解决:用
AtomicReference
包裹对象,保证对象引用的原子性
- 自旋开销:
- 风险:竞争激烈时,线程长时间自旋导致 CPU 占用率飙升
- 优化:结合「自适应自旋」(JVM 动态调整自旋次数)或切换为锁机制
对象头:JVM 实现锁升级的「状态寄存器」
Java 对象头(Object Header)是 JVM 管理对象的核心数据结构,64 位系统中占 16 字节(8 字节 MarkWord + 8 字节 Class Pointer),数组对象额外包含 4 字节数组长度。
核心组成
- MarkWord(8 字节):
- 无锁状态:存储 HashCode(25 位)、分代年龄(4 位)、锁状态(1 位偏向锁标志 + 2 位锁状态)
- 偏向锁:存储当前持有锁的线程 ID(54 位)、分代年龄(4 位)、锁状态(2 位)
- 轻量级锁:存储指向线程栈中锁记录的指针(62 位)、锁状态(2 位)
- 重量级锁:存储指向操作系统互斥锁的指针(62 位)、锁状态(2 位)
- Class Pointer:指向对象的 Class 元数据,用于判断对象类型
- Array Length(数组专有):记录数组长度,非数组对象无此字段
锁状态变化
- 偏向锁(无竞争):首次加锁时记录线程 ID,后续直接复用(零开销)
- 轻量级锁(轻度竞争):通过 CAS 自旋尝试获取锁,避免线程阻塞
- 重量级锁(激烈竞争):自旋超时后升级为 OS 级锁,线程进入阻塞队列
volatile:轻量级可见性与有序性保障
volatile
是 Java 中轻量级的并发关键字,专门解决多线程环境下的变量可见性和指令重排序问题。
两大核心保障
- 编译期:插入内存屏障(Memory Barrier)
- 写屏障:确保 volatile 写操作前的所有指令已执行完毕,且结果对后续操作可见
- 读屏障:确保 volatile 读操作后的所有指令在读取之后执行
- 禁止重排序:编译器无法调整 volatile 读写操作与其他指令的相对顺序
- 运行期:CPU 级数据同步
- 在 x86 架构下,JIT 会为 volatile 写操作生成
lock addl $0, (%esp)
指令(无实际计算,仅触发锁机制) - 锁缓存行:锁定目标缓存行,刷回主存并广播失效事件
- 强制读取主存:其他线程检测到缓存失效后,必须从主存重新加载数据
典型使用场景
- 状态标记变量:如
volatile boolean running = true;
(线程安全的停止标志) - 单例模式 DCL 优化:
public class Singleton {
private static volatile Singleton instance; // 禁止指令重排序,避免返回未初始化对象
// ...
}
- 注意:volatile 不保证原子性(如
i++
需配合AtomicInteger
)
synchronized:从「偏向」到「重量级」的全链路锁升级
synchronized
是 JVM 内置的同步机制,通过「锁升级」策略在不同竞争场景下动态调整锁状态,兼顾性能与正确性。
1. 偏向锁(无竞争场景)
- 核心逻辑:首次进入同步块时,在 MarkWord 中记录当前线程 ID,后续访问直接对比线程 ID(无需真实加锁)
- 升级触发:当其他线程尝试获取锁时,通过 CAS 竞争失败,偏向锁升级为轻量级锁
- 可见性保障:依赖 synchronized 块退出时的「隐式内存屏障」,强制刷新主存数据
2. 轻量级锁(轻度竞争)
- 核心逻辑:
- 线程在栈中创建「锁记录」,存储对象头 MarkWord 的副本
- 通过 CAS 将 MarkWord 替换为指向锁记录的指针(获取锁)
- 竞争失败则自旋重试(默认自旋 10 次,JVM 可动态调整)
- 优势:避免线程挂起的上下文切换开销,适合短时间竞争场景
3. 重量级锁(激烈竞争)
- 核心逻辑:自旋超时后,通过
park()
方法将线程挂起,放入操作系统的等待队列 - 释放逻辑:解锁时调用
unpark()
唤醒线程,触发上下文切换(开销较大) - 适用场景:锁竞争激烈、持有锁时间较长的场景
锁优化最佳实践
- 缩小同步范围:仅在必要代码块加锁(如用
synchronized(this)
替代synchronized(class)
) - 结合显式锁:使用
ReentrantLock
的tryLock()
避免永久阻塞
final:构造函数内的「重排序封印」
final
关键字不仅表示「变量不可变」,更在 JMM 层面提供了底层保障,确保其他线程不会读取到未初始化的 final 变量。
两大底层机制
- 编译器约束:
- 禁止将 final 变量的初始化操作移到构造函数之外
- 非 final 变量可能在构造函数外初始化(如默认值初始化)
- 处理器约束:
- 构造函数结束前,禁止将对象引用(
this
)暴露给其他线程 - 通过内存屏障,确保 final 变量赋值操作在构造函数内完成
反模式警示
public class FinalProblem {
final int x;
static FinalProblem instance;
public FinalProblem() {
x = 10;
instance = this; // 危险!若此时被其他线程读取,x 可能未初始化(JMM 禁止此行为)
}
}
JMM 保证:只要通过正常构造函数初始化,其他线程看到的 final 变量必定是已赋值的状态,避免「半初始化对象」问题。
总结:JMM 核心技术图谱
JMM 三大核心问题:
可见性 → volatile(内存屏障 + lock 指令)、synchronized(锁释放 / 获取的内存语义)、final(构造函数内初始化)
原子性 → synchronized(互斥锁)、CAS(cmpxchg 指令 + 自旋)
有序性 → volatile(禁止重排序)、synchronized(管程进入 / 退出的有序性)
底层硬件机制:
CPU 缓存 → 伪共享(@Contended 注解填充缓存行)
指令重排序 → 编译器重排序(优化代码)+ 处理器重排序(流水线优化)
汇编指令 → lock(缓存行锁定,保障可见性 + 有序性)、cmpxchg(CAS 原子操作)
关键字对比:
关键字 | 可见性 | 原子性 | 有序性 | 锁机制 | 适用场景 |
volatile | ✅ | ❌ | ✅ | 无 | 状态标记、轻量同步 |
synchronized | ✅ | ✅ | ✅ | 锁升级 | 临界区保护 |
final | ✅ | — | ✅ | 无 | 不可变变量 |
并发工具底层:
AQS → CAS(cmpxchg)+ volatile(state 变量)+ 双向链表(等待队列)
原子类 → Unsafe.compareAndSwapXXX(底层 cmpxchg 指令)