最近对Java的volatile关键字做了一些研究,并且做了系列实验做出一些总结若有不对的地方请指正。
volatile这个应该是java中最不好理解的关键字之一,下面少将概念,多以代码和实验结果去理解volatile。
下面从为什么要引入volatile(why),volatile原理(what)对其两方面理解。
实验环境
wind10,openjdk8,idea,hsdis插件
什么要引入volatile
两个线程A、B共享变量a,当线程A对线程a变量该修改了,B线程无法感知到。
public class TestVolatile1 {
public static boolean stop=false;
public static void testStop() {
while (!stop){
}
}
public static void stop(){
stop=true;
}
public static void main(String[] args) {
Thread t1 = new Thread(()-> {
testStop();
} );
Thread t2= new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("主程序退出");
}
}
结果会发现,线程t1一直在循环,无法感知到线程t2对公共变量stop的循环。
修改代码将stop加上volatile。
public class TestVolatile1 {
public static volatile boolean stop=false;
public static void testStop() {
while (!stop){
}
}
public static void stop(){
stop=true;
}
public static void main(String[] args) {
Thread t1 = new Thread(()-> {
testStop();
} );
Thread t2= new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("主程序退出");
}
}
主线程正确退出。
通过以上实验可以证明volatile可以让多线程间可见性得以保证。
volatile语义
-
在java语言中volatile有两层语义
1.保证共享变量的可见性
2.禁止指令重排 -
在C语言中volatile只有一层语义
1.保证共享变量的可见性
实现原理
1.可见性如何保证
cpu层面的保证,缓存一致性协议如mesi协议。
编译器层面,编译成机器指定每次从内存取值,不用寄存器缓存值,每次从内存里取数据。
2.保证顺序性
防止jvm和操作系统层面的指令重排
在存储volatile变量写入后lock add DWORD PTR [rsp],0x0;
0x00000000030546e9: mov edi,0x1
0x00000000030546ee: mov BYTE PTR [rsi+0x69],dil
0x00000000030546f2: lock add DWORD PTR [rsp],0x0 ; com.iboxpay.concurrent.TestVolatile1::stop@5 (line 16
为什么JVM中要加入禁止指令重排指令的语义
因为JVM在做程序优化的做指令重排可能导致共享变量的不可见性。
还是代码1,使用hsdis插件,从汇编层面分析jit生成的机器码
-Xcomp
-XX:+UnlockDiagnosticVMOptions
-XX:PrintAssemblyOptions=intel,hsdis-help
-XX:OnStackReplacePercentage=1400000000
-XX:CompileCommand=dontinline,*TestVolatile1.testStop
-XX:CompileCommand=compileonly,*TestVolatile1.testStop
-XX:CompileCommand=dontinline,*TestVolatile1.stop
-XX:CompileCommand=compileonly,*TestVolatile1.stop
-XX:+PrintAssembly
-XX:+LogCompilation
-XX:LogFile=TestVolatile1.log
可以看出在while循环到一定次数后,C2生成了机器码死循环一直在做安全点检查,
不知道这个是否是hotspot虚拟机故意为之,还是其中存在缺陷。
为此测试了下jdk10配置了Graal编译器,生成了同样的死循环。
XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
将即时编译的级别调到1,程序可以正常退出,主要是C2编译器的优化导致了死循环。
-XX:TieredStopAtLevel=1
代码2加入上volatile属性,java代码对应的汇编如下
由上看出程序可以正常退出。
其实在while循环中加入volatile也可以防止其空循环
public class TestVolatile1 {
public static boolean stop=false;
long p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11;
public static volatile boolean stop2=false;
public static void testStop() {
boolean temp_stop=false;
while (!stop){
temp_stop=stop;
}
}
public static void stop(){
stop=true;
stop2=true;
}
public static void main(String[] args) {
Thread t1 = new Thread(()-> {
testStop();
} );
Thread t2= new Thread(()->{
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
}catch (Exception e){
e.printStackTrace();
}
}
}
java对应的机器码
总结
作用
volatile保证多线程共享变量的可见性
实现原理
cpu层面,有缓存一直性协议保证,如x86
编译层面,机器码直接使用内存值寄存器不缓存
jvm及时编译, 如x86 lock add DWORD PTR [rsp],0x0,防止指令重排,刷新缓存。