在 Java 中,volatile 还有一个重要的作用就是防止 JVM 的指令重排序,如果将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序
在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
public native void loadFence();
public native void storeFence();
public native void fullFence();
理论上来说,通过这个三个方法也可以实现和 volatile 禁止重排序一样的效果,只是会麻烦一些
禁止指令重排效果演示
// 双重检查实现单例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
由于 JVM 具有指令重排的特性,执行顺序有可能变成 1 ⇒ \Rightarrow ⇒ 3 ⇒ \Rightarrow ⇒ 2
指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例
例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的
假设对变量 inc 进行 ++ 操作,inc++ 其实是一个复合操作,包括三步:
读取 inc 的值
对 inc 加 1
将 inc 的值写回内存
volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
线程 1 对 inc 进行读取操作之后,还未对其进行修改
线程 2 又读取 inc 的值并对其进行修改(+1),再将 inc 的值写回内存
线程 2 操作完毕后,线程 1 对 inc 的值进行修改(+1),再将 inc 的值写回内存
这就导致两个线程分别对 inc 进行一次自增操作后,inc 实际上只增加 1
如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、Lock 或者 AtomicInteger 都可以
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种:源代码 ⇒ \Rightarrow ⇒ 编译器优化的重排 ⇒ \Rightarrow ⇒ 指令并行的重排 ⇒ \Rightarrow ⇒ 内存系统的重排 ⇒ \Rightarrow ⇒ 最终执行指令
单线程环境最终执行结果和代码顺序的结果一致
处理器在进行重排序时,必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
volatile 实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:保证特定操作的顺序 和 保证某些变量的内存可见性
1、由于编译器和处理器都能执行指令重排的优化,如果在程序中插入一条Memery Barrier(内存屏障),那么就会告诉编译器和CPU不能对这条指令进行重排
2、也就是说通过插入内存屏障,使屏障前后的指令不会进行重排优化
3、内存屏障还可以强制刷出CPU的缓存,因此CPU上的线程都能读到这些数据的最新版本
内存屏障分为四种
StoreStore 屏障、StoreLoad 屏障、LoadLoad 屏障、LoadStore 屏障
Load 相当于读屏障,Store 相当于写屏障
工作流程