`volatile`和`synchronized`这两个Java常用关键字是如何借助内存屏障实现其同步语义
你希望深入理解JVM内存屏障的核心概念,同时具体掌握volatile和synchronized这两个Java常用关键字是如何借助内存屏障实现其同步语义的,我会从底层机制到实际应用示例,清晰拆解这部分内容。
一、JVM内存屏障核心回顾
内存屏障(Memory Barrier)是JVM基于CPU指令抽象出的底层机制,核心解决多线程场景下的两个问题:
- 指令重排序:禁止屏障两侧的指令被编译器/CPU重排,保证执行顺序;
- 内存可见性:强制缓存与主内存同步(写屏障刷缓存到主内存,读屏障从主内存加载数据)。
JVM定义了4种核心内存屏障(按开销从低到高):
| 屏障类型 | 核心作用 |
|---|---|
| LoadLoad | 禁止屏障前的读操作与屏障后的读操作重排序 |
| StoreStore | 禁止屏障前的写操作与屏障后的写操作重排序 |
| LoadStore | 禁止屏障前的读操作与屏障后的写操作重排序 |
| StoreLoad | 禁止屏障前的写操作与屏障后的读操作重排序(最重量级) |
Java程序员无需直接操作内存屏障,JVM会为volatile、synchronized等关键字自动插入对应的屏障。
二、volatile如何利用内存屏障
volatile是轻量级同步关键字,能保证变量的可见性和有序性(不保证原子性),其核心就是JVM为volatile变量的读写插入特定内存屏障。
1. volatile的内存屏障插入规则
| 操作类型 | 屏障插入位置 | 插入的屏障类型 | 核心目的 |
|---|---|---|---|
| 写volatile变量 | 写操作前 | StoreStore屏障 | 保证普通变量的写操作先刷到主内存,再执行volatile写 |
| 写volatile变量 | 写操作后 | StoreLoad屏障 | 保证volatile写刷到主内存,且后续读/写不重排到volatile写之前 |
| 读volatile变量 | 读操作前 | LoadLoad屏障 | 保证普通变量的读操作完成后,再执行volatile读 |
| 读volatile变量 | 读操作后 | LoadStore屏障 | 保证volatile读完成后,后续写操作不重排到volatile读之前 |
2. 实战示例1:volatile解决单例模式指令重排序问题
这是volatile最经典的应用场景,核心解决“双重检查锁(DCL)”中对象半初始化问题:
public class Singleton {
// 必须加volatile!否则可能出现指令重排序导致空指针
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:避免不必要的锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:防止并发创建
// new操作的实际指令分3步(不加volatile会重排):
// 1. 分配内存 → 2. 初始化对象 → 3. 给instance赋值
// 重排后可能变成 1→3→2,导致其他线程读到"非null但未初始化"的instance
// 加volatile后,StoreStore/StoreLoad屏障禁止这种重排序
instance = new Singleton();
}
}
}
return instance;
}
}
屏障作用解析:
- 写
instance(instance = new Singleton())时,JVM先插入StoreStore屏障,禁止“初始化对象”和“赋值”重排; - 再插入
StoreLoad屏障,保证赋值完成后,其他线程读instance能拿到主内存的最新值。
3. 实战示例2:volatile读写的屏障执行流程
public class VolatileDemo {
private volatile int flag = 0; // volatile变量
private int normalNum = 0; // 普通变量
// 写volatile:触发StoreStore + StoreLoad屏障
public void write() {
normalNum = 10; // 普通写
// 插入StoreStore屏障:保证normalNum的写先刷到主内存,再执行flag的写
flag = 1; // volatile写
// 插入StoreLoad屏障:保证flag的写刷到主内存,后续操作不重排到这里之前
}
// 读volatile:触发LoadLoad + LoadStore屏障
public void read() {
// 插入LoadLoad屏障:保证之前的读操作完成,再读flag
int temp = flag; // volatile读
// 插入LoadStore屏障:保证读flag完成后,再执行normalNum的写
normalNum = temp + 1; // 普通写
}
}
三、synchronized如何利用内存屏障
synchronized是重量级同步机制,能保证原子性、可见性、有序性,其内存屏障插入逻辑覆盖整个同步块,比volatile更全面。
1. synchronized的内存屏障插入规则
synchronized的屏障插入分为“进入同步块”和“退出同步块”两个阶段:
| 操作阶段 | 屏障插入位置 | 插入的屏障类型及作用 |
|---|---|---|
| 进入同步块 | 获取锁后、执行块内代码前 | LoadLoad + LoadStore屏障: 1. 禁止块内操作重排到获取锁之前; 2. 强制从主内存加载共享变量最新值 |
| 退出同步块 | 释放锁前、执行块内代码后 | StoreStore + StoreLoad屏障: 1. 禁止块内操作重排到释放锁之后; 2. 强制将块内修改刷到主内存 |
2. 实战示例:synchronized的屏障效果
public class SynchronizedDemo {
private int count = 0; // 普通共享变量
// 同步方法(等价于synchronized(this))
public synchronized void increment() {
// 进入同步块:JVM插入LoadLoad + LoadStore屏障
// 1. 强制从主内存读取count的最新值(可见性)
// 2. 禁止count++重排到获取锁之前(有序性)
count++; // 原子性:同步块保证该操作不会被其他线程打断
// 退出同步块:JVM插入StoreStore + StoreLoad屏障
// 1. 强制将count的修改刷到主内存(可见性)
// 2. 禁止count++重排到释放锁之后(有序性)
}
// 同步代码块
public void decrement() {
synchronized (this) {
// 进入/退出时的屏障逻辑与同步方法完全一致
count--;
}
}
}
核心效果:
- 线程1执行完
synchronized块后,修改的count会通过StoreStore/StoreLoad屏障刷到主内存; - 线程2进入同一个
synchronized块时,通过LoadLoad/LoadStore屏障从主内存读取最新的count值,保证多线程数据一致。
3. volatile vs synchronized 内存屏障对比
| 特性 | volatile | synchronized |
|---|---|---|
| 屏障作用范围 | 仅针对自身变量的读写 | 覆盖整个同步块的所有操作 |
| 性能开销 | 轻量级(无锁竞争) | 重量级(可能触发上下文切换) |
| 保证的语义 | 可见性、有序性(无原子性) | 原子性、可见性、有序性 |
总结
- 内存屏障是JVM解决重排序和可见性的底层核心,分为4种类型,
StoreLoad屏障开销最大; volatile通过在变量读写时插入特定屏障,实现可见性和有序性,典型场景是DCL单例模式;synchronized通过在同步块进入/退出时插入组合屏障,覆盖块内所有操作,实现全量同步语义(原子性+可见性+有序性)。
关键点回顾
volatile的屏障仅作用于自身变量,轻量但不保证原子性;synchronized的屏障覆盖整个同步块,重量级但能保证原子性;- 两者均由JVM自动插入内存屏障,无需程序员手动操作。

浙公网安备 33010602011771号