目录
四、Volatile 与内存屏障 Memory Barrier
一、Volatile可见性问题
public class TestVolatileVisibility {
public static /* volatile */int found = 0;
public static void main(String[] args) {
new Thread(() -> {
System.out.println("A waiting money,Begin...");
// 加上 volatile 能让每次都去内存读取数据到CPU cache
while (0 == found){
}
System.out.println("A get the money, End ...");
},"myThread-A").start();
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (Exception e){
e.printStackTrace();
}
System.out.println("B send money");
change();
},"myThread-B").start();
}
public static void change(){
found = 1;
}
}
输出:
A waiting money,Begin...
B send money
A get the money, End ...
如果不加关键字 volatile,那么A线程将一直等待,因为A线程的found一直是去cpu缓存中读取,不会去内存中重新刷出来。从而导致一直 while循环。
volatile保证了可见性,能保证每次都能去内存读,而不单单取CPU缓存中的数据。
二、Idea查看字节码
下载插件: jclasslib Bytecode viewer
或者直接 用javap -v -p class文件名(不要.class 后缀)
运行
IDEA -- Reference -- Plugins
先Build一下,生成Class文件
View -- Show Bytecode With Jclasslib
可以发现,加了 Volatile的时候,从字节码中,可以看到 Access flags中是有 volatile,
自己去比对就知道,java代码中,其他地方都没区别,加不加 volatile,在字节码层面只在 Access flags这个地方有区别。
三、查看汇编代码
Mac下查看已安装的jdk版本及其安装目录
localhost:bin jeblin$ /usr/libexec/java_home -V
Matching Java Virtual Machines (1):
1.8.0_231, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home
工具 : hsdis-amd64.dylib
将上述两个文件放在你的 jre/bin 路径下的路径里。
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/hsdis-amd64.dylib
idea配置 VM options:
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*TestVolatileVisibility.change
这块(TestVolatileVisibility.change)换成你的类名+方法名字
然后运行,或者直接命令行运行Java
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*VolatileBarrierExample.readAndWrite 包名.类名
0x00000001128318ad: lock addl $0x0,(%rsp) ;*putstatic found
; - indi.sword.util.basic.Thread.Volatile.TestVolatileVisibility::change@1 (line 31)
如果没有Volatile关键字,那么就没有 lock
0x0000000109ab3565: movl $0x1,0x68(%rsi) ;*putstatic found
; - indi.sword.util.basic.Thread.Volatile.TestVolatileVisibility::change@1 (line 31)
可见其本质是通过一个lock指令来实现的。那么lock是什么意思呢?
lock是一种控制指令,在多处理器环境下,lock 汇编指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果。
查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。(我的理解是通知shared状态的cpu强制去消费invalid queue队列,把位于cpu Cache中的共享的变量 CacheLine置为Invalid,然后重新去内存中读取最新的值)所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。
也就是Volatile解决了可见性的问题
所以,它的作用是
- 锁住主存
- 任何读必须在写完成之后再执行
- 使其它线程这个值的栈缓存失效
类似于前面是storestore
,后面是storeload
四、Volatile 与内存屏障 Memory Barrier
Volatile使用内存屏障,内存屏障的作用,是为了阻止各阶段的乱序优化。
Java内存模型关于重排序的规定,总结后如下表所示:
表格中的“是否允许重排序”,意思是指,第一项操作和第二项操作会不会被重排序,也就是这俩操作会不会被乱序优化。
大神 Doug Lea 写的一篇Java内存屏障说明:
竖的是第一个操作,横的是第二个操作。
LoadLoad是啥含义? 左边的Load表示第一个操作是读,右边的Load表示第二个操作是读。其他以此类推。
Required barriers | 2nd operation | |||
1st operation | Normal Load | Normal Store | Volatile Load MonitorEnter | Volatile Store MonitorExit |
Normal Load | LoadStore | |||
Normal Store | StoreStore | |||
Volatile Load MonitorEnter | LoadLoad | LoadStore | LoadLoad | LoadStore |
Volatile Store MonitorExit | StoreLoad | StoreStore |
Here's an example showing placements.
Java | Instructions |
class X { int a, b; volatile int v, u; void f() { int i, j; i = a; j = b; i = v; j = u; a = i; b = j; v = i; u = j; i = u; j = b; a = i; } } | load a load b load v LoadLoad load u LoadStore store a store b StoreStore store v StoreStore store u StoreLoad load u LoadLoad LoadStore load b store a |
从上表我们可以看出:
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。(也就是Volatile 读完之后,才去干后面的事情)
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。(也就是Volatile写之前,前面事情已经干完了)
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。(写的第2条已经说了,不能)