volatile关键字
用法
修饰需要多个线程共享的变量,例如静态变量或实例变量(多个线程共享该实例变量,可能同时修改和读取它)
作用
- 保证内存可见性
- 防止指令重排序
验证
程序验证
可见性验证
程序示例如下:
java
package com.jvm;
public class TestVolatile {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
while (!stop) {
// try {
// Thread.sleep(1000);
// }
// catch (InterruptedException e) {
// e.printStackTrace();
// }
}
System.out.println("loop end");
});
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
});
thread1.start();
thread2.start();
}
}
上面的程序有两个线程在访问静态变量stop
,程序输出结果是:一直处于循环中,并且永远不会输出loop end
字符串,因为线程2修改了stop
值,但是线程1不会知道,因为线程2修改的只是它的缓存
中的值,而线程1读取的值也只是它的缓存
中的值。具体解释参考下面内容。
如果将stop
前面加上volatile
,则程序会退出循环,并输出loop end
字符串,这正是volatile
保证内存可见性的作用。
有个小插曲,写这段程序测试时,刚开始线程1中Thread.sleep
方法没有被注释,想着只是为了循环慢一点,但是当运行时,发现当线程1输出了几次loop
字符串时,意外输出了loop end
字符串,经过多次调试与查资料,Thread.sleep
可能会导致线程1观察到修改后的stop
变量的值,具体不深究了。
2.
汇编指令验证
通过安装hsdis工具,并配置下面的VM参数,则可以在程序运行后查看到程序对应的汇编指令:
java
-XX:+UnlockDiagnosticVMOptions
// 打印汇编代码
-XX:+PrintAssembly
-Xcomp
-XX:+LogCompilation
-XX:LogFile=D:\jvmcompilelog\hotspot.log
// 代表只编译TestVolatile类的change方法
-XX:CompileCommand=compileonly,*TestVolatile.change
change
方法是一个修改被volatile
修饰的变量的值的方法,如下:
java
private static void change() {
stop = true;
}
编译后输出的汇编指令如下:
java
0x000001f3eb4701ea: movabs $0x720b25180,%rsi ; {oop(a 'java/lang/Class'{0x0000000720b25180} = 'com/jvm/TestVolatile')}
0x000001f3eb4701f4: mov $0x1,%edi
0x000001f3eb4701f9: mov %dil,0x88(%rsi)
0x000001f3eb470200: lock addl $0x0,-0x40(%rsp) ;*putstatic stop {reexecute=0 rethrow=0 return_oop=0}
; - com.jvm.TestVolatile::change@1 (line 99)
其中lock addl $0x0,-0x40(%rsp)
正是volatile
汇编后的关键指令,这个指令中addl $0x0,-0x40(%rsp)
是将rsp寄存器的值和0相加,lock
是一个指令前缀,被它修饰的操作在执行时会独占共享内存。
lock
前缀指令有两个很重要的作用:
- 当执行该指令时,会把CPU缓存中的数据刷新到主内存,并使其他CPU缓存中缓存的该数据失效
- 防止该指令的前后指令进行重排序,也就类似内存屏障的作用
正因为它的这两个作用,所以当volatile
修饰的变量被修改后,其他线程能立即观察到修改后的值,因为其他线程中缓存的该变量的值失效了,必须从主存中重新加载。
指令重排序
定义
CPU会根据性能需要对指令进行乱序执行,但是在单线程内保证按照as-if-serial语义执行,其实就是保证乱序执行后的结果和单线程中程序定义的顺序执行结果一致。
问题
乱序执行在单线程中能保证程序正常执行,但是在多线程中不能保证,所以需要使用内存屏障
来保证特定顺序。
内存屏障
定义
内存屏障主要是一种指令,根据类型的不同,实现不同的效果,用来防止该指令前后的指令进行重排序和刷新CPU缓存数据到主存。
分类
汇编语言
- sfence
store fence:写屏障,保证写屏障之前的写入操作可见性先于写屏障之后的写入操作的可见性。 - lfence
load fence: 读屏障,保证读屏障之前的加载操作先于读屏障之后的加载操作,并且可以保证读屏障之前的加载操作序列化执行。 - mfence
memory fence: 内存屏障,保证内存屏障之前的读写操作可见性先于内存屏障之后的读或写操作,并且保证内存屏障之前的读写操作序列化执行。
JVM层面
JSR定义了四种内存屏障来屏蔽不同硬件平台的内存屏障实现。
- LoadLoad:防止两个读取操作重排序。
- LoadStore:防止读和写操作重排序。
- StoreStore:防止两个写操作重排序。加入指令1是将x的值设置为1,指令2是将y的值设置为true,一个线程监控y的值,当为true时,读取x的值,如果指令1和2重排序,会导致线程读取到x的值不为1。
- StoreLoad:防止写和读重排序,并且保证写入的数据刷新到主存。
Happened-Before关系
作用
JMM用来描述内存可见性的高级概念。满足该关系的两个操作能保证内存可见性。
该关系有几个原则,每种原则描述的都是该关系之前的操作对于该关系之后的操作可见。
并且该关系具有传递性,如果A 先于 B,B 先于 C,则A 先于 C,那么A的操作结果对C可见。