回答
指令重排序是指在执行程序时,为了提高性能,处理器可能会改变指令的执行顺序。
volatile
通过内存屏障来防止指令重排序,保证有序性。volatile
提供了两种内存屏障:
- 写内存屏障:写内存屏障设置在写
volatile
变量之后。它确保对该volatile
变量的所有写操作在任何后续对同一变量的读操作之前完成。这就意味着,在写内存屏障之前的所有普通写操作(不仅仅是对volatile
变量的写操作)都将在写入volatile
变量之前完成。 - 读内存屏障:读内存屏障设置在读
volatile
变量之前。它确保对该volatile
变量的所有读操作在任何先前的写操作之后完成。这就意味着,所有在读内存屏障之后的普通读操作(不仅仅是对volatile
变量的读操作)都将在读取volatile
变量之后进行。
这种内存屏障阻止了指令重排序,确保在 volatile
变量之前的操作不会被重排序到其之后,同时也确保在 volatile
变量之后的操作不会被重排序到其之前。
详细分析
指令重排序
为了提高应用程序的性能,编译器和处理器都会我们的指令按照某种规则重新排序。简单来说就是指我们在程序中写的代码,在执行时并不一定按照写的顺序执行**。**比如下面这段代码:
int i = 0; // 1
boolean flag = false; // 2
String str = "死磕 Java"; // 3
按照我们常规的思路,程序执行的顺序就是 1 —> 2 —> 3
,但是在程序运行时,执行顺序就无法确认了,可能是 1 —> 3 —> 2
,也有可能是 3 —> 2 —> 1
等等。
但是无论如何指令重排序,它都不能改变程序最终的运行结果,例如:
int i = 1; // 1
int k = i + 3; // 2
int m = i + 2 * k; // 3
这种情况下能否进行指令重排序?显然不能,如果你改变他们的执行顺序会导致程序运行结果的变化。所以指令重排序要遵循如下几个规则:
- 程序顺序规则:对于单线程来说,重排序后的程序执行结果应该与该程序代码顺序执行的结果一致。也就是在单线程环境下不能改变程序运行的结果。
- 数据依赖性规则:重排序过程中必须保持数据之间的依赖性。
指令重排序有 3 种
- 编译器重排序:编译器在不改变单线程程序的语义前提下,可以重新安排语句的执行顺序。它是源代码级别。
- 处理器重排序:处理器可能会基于其内部逻辑,如指令流水线、执行单元的可用性等因素在执行时对指令进行重排序。
- 内存重排序:由于目前都是多核处理器和多级缓存,内存操作的执行顺序可能会与程序中的顺序不同。
as-if-serial 语义
as-if-serial
语义是:所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变。上面所提到的程序顺序规则和数据依赖性规则就是 as-if-serial
的语义。简单举个例子:
int a = 1 ; // A
int b = 2 ; // B
int c = a + b; // C
A、B、C三个操作存在如下关系:A、B不存在数据依赖关系,A 和C、B 和 C 存在数据依赖关系,因此在进行重排序的时候,A、B可以随意排序,但是必须位于 C 的前面,执行顺序可以是A —> B —> C
或者B —> A —> C
。但是无论是何种执行顺序最终的结果 C 总是等于3。 as-if-serail
语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。
as-if-serail
保证的是单线程环境下的,那多线程呢?这就要依赖 happens-before
原则了。
happens-before
happens-before
定义了在多线程环境中,内存的写操作和读操作之间的顺序关系。它确保了多线程环境下,线程间共享变量的操作是有序且可见的。即:
在 Java 内存模型中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在
happens-before
关系。
happens-before 原则有如下八大原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则:一个 unLock 操作先行发生于后面对同一个锁额 lock 操作。
- volatile变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
- 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C。
- 线程启动规则:Thread 对象的
start()
方法先行发生于此线程的每个一个动作。 - 线程中断规则:对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 - 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过
Thread.join()
方法结束、Thread.isAlive()
的返回值手段检测到线程已经终止执行。 - 对象终结规则:一个对象的初始化完成先行发生于他的
finalize()
方法的开始。
如果两个操作不满足上述八大原则中的任意一个,那么这两个操作就没有顺序保证,虚拟机可以对这两个操作进行重排序。如果操作 A happens-before 操作 B,那么 A 在内存所做的修改对 B 都是可见的。
关于 happens-before 原则,大明哥这篇文章有非常详细的介绍:
https://www.skjava.com/series/article/1711591604
volatile 使用内存屏障保证有序性
什么是内存屏障
什么是内存屏障?内存屏障其实就是一个 CPU 指令,我们可以理解它是一种指令级别的同步机制,它用于确保指令的执行顺序以及内存操作的可见性和顺序性。
从硬件层面来说,内存屏障分为两种:
- Load Barriers:加载屏障,确保所有在 Load Barriers 屏障之前的读操作完成后,才执行屏障之后的读操作。
- Store Barriers:存储屏障,确保所有在 Store Barriers** **屏障之前的写操作完成后,才执行屏障之后的写操作。
从 JVM 层面来说,内存屏障分为四种:
- LoadLoad
- 确保 LoadLoad 屏障之前的所有加载操作(Load)在内存中完成后,才能执行屏障之后的加载操作。
- 例如:Load1; LoadLoad; Load2,保证 load1 的读操作先于 load2 执行。
- StoreStore:
- 确保 StoreStore 屏障之前的所有存储操作(Store)在内存中完成后,才能执行屏障之后的存储操作。
- 例如:Store1; StoreStore; Store2,保证 store1 的写操作先于 store2 执行,并刷新到主内存。
- LoadStore:
- 确保 LoadStore 屏障之前的加载操作在内存中完成后,才能执行屏障之后的存储操作。
- 例如:Load1; LoadStore; Store2,保证 load1 的读操作结束先于 load2 的写操作执行。
- StoreLoad:
- 最强内存屏障,确保 StoreLoad 屏障之前的所有存储操作完成后,才能执行屏障之后的加载操作。
- 例如:Store1; StoreLoad; Load2,保证 store1 的写操作已刷新到主内存之后,load2 及其后的读操作才能执行。
volatile 是如何使用内存屏障保证有序性的?
Volatile
通过内存屏障可以禁止指令重排序,保证了操作的有序性,其规则如下:
- 如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
- 当第二个操作为volatile写是,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
- 当第一个操作volatile写,第二操作为volatile读时,不能重排序。
图例如下:
那是插入什么内存屏障来保持的呢?
- 在每一个
volatile
写操作前面插入一个StoreStore
屏障,禁止上面的普通写与volatile
写重排序。 - 在每一个
volatile
写操作后面插入一个StoreLoad
屏障,禁止volatile
写于后面的可能存在的volatile
读/写重排序 - 在每一个
volatile
读操作后面插入一个LoadLoad
屏障,禁止volatile
读与下面的普通读重排序。 - 在每一个
volatile
读操作后面插入一个LoadStore
屏障,禁止volatile
读与下面的普通写重排序。
下面用一个例子来说明下:
public class VolatileTest {
int i = 0;
volatile boolean flag = false;
public void write(){
i = 2;
flag = true;
}
public void read(){
if(flag){
System.out.println("---i = " + i);
}
}
}
内存屏障如下:
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。