Java 内存模型
Java内存模型规定在多线程情况下,线程操作主内存(类比内存条)变量,需要通过线程独有的工作内存(类比CPU高速缓存)拷贝主内存变量副本来进行。此处的所谓内存模型要区别于通常所说的虚拟机堆模型,可以参考如下:
需要注意的是如果是一个大对象,并不会从主内存完全拷贝一份,而是这个被访问对象引用的对象、对象中的字段可能存在拷贝。线程独有的工作内存和进程内存(主内存)之间通过几种原子操作来实现,如下所示:
volatile 禁止指令重排序
volatile变量的禁止指令重排序是基于内存屏障(Memory Barrier)实现。内存屏障又称内存栅栏,是一个CPU指令,内存屏障会导致JVM无法优化屏障内的指令集。
- 对
volatile变量的写指令后会加入写屏障,对共享变量的改动,都同步到主存当中 - 对
volatile变量的读指令前会加入读屏障,对共享变量的读取,加载的是主存中最新数据
屏障位置对比如下:
// 左:volatile 写操作 右:volatile 读操作
public class BarrierPositionComparison {
// 写操作:屏障在写之后 读操作:屏障在读之前
// 普通写 // LoadLoad屏障
// volatile 写 // LoadStore屏障
// StoreStore屏障 // volatile 读
// StoreLoad屏障 // 后续操作
}
在JVM主要存在如下四种内存屏障
// JMM 定义的四种屏障
public class MemoryBarriers {
// 1. LoadLoad 屏障
// 确保 Load1 数据的装载先于 Load2 及其后所有装载指令
// 序列:Load1 → LoadLoad → Load2
// 2. StoreStore 屏障
// 确保 Store1 及其前面的写操作数据刷新到内存先于 Store2
// 序列:Store1 → StoreStore → Store2
// 3. LoadStore 屏障
// 确保 Load1 的数据装载先于 Store2 及其后所有存储指令
// 序列:Load1 → LoadStore → Store2
// 4. StoreLoad 屏障(全能屏障,开销最大)
// 确保 Store1 及其前面的写操作对其他处理器可见先于 Load2
// 序列:Store1 → StoreLoad → Load2
}
涉及到的屏障插入伪代码可以参考如下:
// 为 volatile 读插入屏障
void insert_barriers_for_volatile_read() {
// volatile 读操作前插入:
OrderAccess::loadload(); // LoadLoad 屏障
OrderAccess::loadstore(); // LoadStore 屏障
// 然后执行实际的读操作
// T value = *address;
// 屏障位置:屏障 → 读操作
}
// 为 volatile 写插入屏障
void insert_barriers_for_volatile_write() {
// 先执行实际的写操作
// *address = value;
// volatile 写操作后插入:
OrderAccess::storestore(); // StoreStore 屏障
OrderAccess::storeload(); // StoreLoad 屏障
// 屏障位置:写操作 → 屏障
}
从上面这个伪代码大致可以得出如下内存屏障定位图:
volatile 写操作的时间线:
[普通写] → [volatile写] → StoreStore屏障 → StoreLoad屏障 → [后续操作]
↑ ↑
写操作完成 屏障在写之后
volatile 读操作的时间线:
[LoadLoad屏障] → [LoadStore屏障] → [volatile读] → [后续操作]
↑ ↑
屏障在读之前 屏障在读之前
在不同的CPU架构中,内存屏障对应的指令也会有所不同,这里仅给出参考。
x86/x64 架构(强内存模型):
// x86 的内存屏障实现相对简单
public class X86BarrierInstructions {
// LoadLoad 屏障:
// 在 x86 上,大多数情况下是 no-op(无操作)
// 因为 x86 保证读操作不会重排序(Load-Load 顺序)
// 对应汇编:通常什么都不需要
// StoreStore 屏障:
// 在 x86 上,通常也是 no-op
// 因为 x86 保证写操作不会重排序(Store-Store 顺序)
// 对应汇编:通常什么都不需要
// LoadStore 屏障:
// 在 x86 上,通常也是 no-op
// 对应汇编:通常什么都不需要
// StoreLoad 屏障:
// 这是 x86 上唯一需要显式指令的屏障!
// 对应汇编:mfence 指令 或 lock 前缀
// mfence 指令:内存屏障指令
// lock 前缀:锁定总线,更重量级
// 总结:x86 上大部分屏障是"无操作",只有 StoreLoad 需要 mfence
}
ARM/POWER 架构(弱内存模型):
// ARM 需要显式的内存屏障指令
public class ARMBarrierInstructions {
// ARM 的所有屏障都需要显式指令!
// LoadLoad 屏障:
// 对应指令:dmb ishld (数据内存屏障,内部共享域,仅对加载有效)
// 或:dmb ish (更强的屏障)
// StoreStore 屏障:
// 对应指令:dmb ishst (数据内存屏障,仅对存储有效)
// 或:dmb ish
// LoadStore 屏障:
// 对应指令:dmb ish (全能屏障)
// ARM 没有单独的 LoadStore 屏障指令
// StoreLoad 屏障:
// 对应指令:dmb ish (全能屏障)
// 注意:ARM 的 dmb ish 是全能屏障,对应所有四种屏障
// 但 JVM 会根据情况选择更精确的屏障类型
}
单例模式懒汉变量加 volatile
问题代码:
// 没有 volatile 的双重检查锁
public class UnsafeSingleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (UnsafeSingleton.class) {
if (instance == null) {
// 这里可能被重排序!
instance = new Singleton();
}
}
}
return instance;
}
}
分解 new Singleton():
// instance = new Singleton(); 分解为:
memory = allocate(); // 1. 分配内存
ctorInstance(memory); // 2. 调用构造函数(初始化字段)
instance = memory; // 3. 赋值给引用
// 可能被重排序为:1 → 3 → 2
// 此时 instance != null,但对象还没初始化!
volatile 如何解决:
private static volatile Singleton instance;
// 有 volatile 时:
instance = new Singleton();
// 编译为:
memory = allocate();
ctorInstance(memory); // 初始化
// StoreStore 屏障(禁止步骤3重排序到步骤2前面)
instance = memory; // volatile 写
// StoreLoad 屏障(保证立即对其他线程可见)
// 其他线程:
if (instance != null) { // volatile 读
// LoadLoad 屏障
// LoadStore 屏障
instance.method(); // 此时对象一定初始化完成
}
注意点
- volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
- 从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
- 在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。
- 加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。
当且仅当满足以下所有条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量没有包含在具有其他变量的不变式中。
下面分别给出符合和不符合volatile变量应用条件的例子:
符合条件的例子
- 对变量的写入操作不依赖变量的当前值:
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public boolean isFlag() {
return flag;
}
}
在这个例子中,flag变量的写入操作不依赖于当前值,只是简单地将其设置为true。这种情况下适合使用volatile修饰。
不符合条件的例子
- 变量的写入操作依赖变量的当前值:
public class NonVolatileExample {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个例子中,count变量的写入操作依赖于当前值的增加。多个线程同时调用increment方法可能会导致竞态条件,因此不适合使用volatile修饰。
- 变量包含在具有其他变量的不变式中:
public class InvariantExample {
private volatile int x = 0;
private int y = 0;
public void updateX() {
x = y + 1;
}
public int getX() {
return x;
}
public void updateY() {
y = 10;
}
public int getY() {
return y;
}
}
在这个例子中,x变量的更新依赖于y变量的值,两者存在关联。如果使用volatile修饰x,可能会导致多线程环境下x和y之间的关系出现问题,不适合使用volatile修饰x。