一、概述
1、特性(内存语义)
当一个变量被声明为 volatile
时,它具备以下两个关键特性:
-
可见性:
volatile
保证了一个线程对该变量的修改,能被其他线程立即看到。也就是说,当一个线程更新了volatile
变量的值,其他线程可以立即感知到这个变化。- 普通变量则不同,线程对其修改后,其他线程无法立即看到,通常需要通过主内存的同步(例如,线程A修改变量后写入主内存,线程B从主内存中读取)才能使更新对其他线程可见。
-
禁止指令重排序:
volatile
禁止了编译器和处理器对该变量相关操作的指令重排序优化。这意味着,volatile
变量的读写操作在程序代码中出现的顺序,与实际执行的顺序是一致的。- 对于普通变量,编译器或处理器可能会为了优化性能而改变指令的顺序,这种重排序不会影响单线程的执行结果,但在多线程环境中可能导致意外行为。而
volatile
则通过内存屏障确保指令的执行顺序符合预期。 - 这种行为符合 Java 内存模型中描述的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),即尽管可能存在指令重排序优化,但在单个线程内,代码的执行结果看起来是按照程序的顺序依次执行的。
volatile在Java语言规范中规定的是
Java内存模型中规定了volatile的happen-before效果,对volatile变量的写操作happen-before于后续的读。这样volatile变量能够确保一个线程的修改对其他线程可见。volatile因为不能保证原子性,所以不能在有约束或后验条件的场景下使用,如i++,常用的场景是stop变量保证系统停止对其他线程可见,double-check lock单例中防止重排序来保证安全发布等。
二、volatile 底层原理(x86架构)
1、说明
volatile关键字修饰的变量可以保证可见性与有序性,无法保证原子性。那么volatile关键字的底层原理是什么呢?我们可以通过查看Java代码的汇编指令去看一下volatile的底层原理。
我们先编写一段使用了 volatile
的代码,并通过循环多次执行该代码,以触发 JIT
(Just-In-Time
编译器)将其编译为机器码。随后,我们可以使用 HSDIS
工具将生成的机器码反汇编为汇编代码,并对这些汇编代码进行分析,探究 volatile
的底层工作机制。
2、HSDIS 工具
(1)说明
HSDIS
(HotSpot Disassembler
)工具可以将 Java 程序在运行时生成的机器码反汇编为更容易理解的汇编代码。这对于分析 JVM
内部指令执行情况尤其有用。
如果jdk版本小于等于8还要在jdk里面添加HSDIS插件。
(2)下载地址
(3)使用方法
-
将下载的 HSDIS 库文件(hsdis-amd64.dll,hsdis-i386.dll)放置在 Java 安装路径下的
jre/bin/server
或jre/bin/client
目录中。 -
在运行 Java 程序时,使用 JVM 参数
-XX:+UnlockDiagnosticVMOptions
和-XX:+PrintAssembly
来启用 HSDIS。
以下是 HSDIS 工具启用后的效果图:
(4)参考
使用JVM的HSDIS插件
https://blog.csdn.net/VimGuy/article/details/81879210在64位Windows上编译hsdis(提供下载)
https://blog.csdn.net/yizishou/article/details/53423409
3、示例代码
下面的代码中,volatile
变量 i
被多次递增。通过大量循环执行该代码,可以触发 JIT
编译器对其进行优化编译。
public class HelloVolatile {
public static volatile int i = 0;
public static void main(String[] args) {
for (int j = 0; j < 100000; j++) {
n();
}
}
public static void n() {
i++;
}
}
另外,我们也可以使用以下 JVM
参数,这样无需多次循环即可强制 JIT
编译:
-XX:+</