禁止指令重排序优化。
public class Monitors {
private static volatile Monitors monitors = null;
private Monitors() {}
public static Monitors getMonitor() {
if (monitor == null) {
synchronized (Monitors.class) {
if (monitors == null) {
monitors = new Monitors();
}
}
}
return monitors;
}
}
-
分析代码
- monitors = new Monitors();,在这个操作中,JVM主要做三件事:
1、在java 堆空间里分配一部分空间;
2、执行 Monitor 的构造方法进行初始化;
3、把 monitor 对象指向在堆空间中分配好的空间。
三步执行完后,这个 monitor 对象就不是空值。
- 普通变量不能保证变量赋值操作的顺序与程序代码中的执行顺序一致,指令重排序的优化很可能改变程序的执行顺序。比如,执行顺序可能为
1、2、3,也可能为1、3、2。如果是按照1、3、2的顺序执行,恰巧在执行到3的时候(还没执行2),新的线程执行 getMonitor() 方法之后判断
monitor 不为空就返回了 monitor 实例。此时 monitor 实例虽有值, 但它没有执行构造方法进行初始化(即没有执行2),
故该线程如果对那些需要初始化的参数进行操作就会发生错误。
但是加volatile 关键字的话,就不会出现这个问题。禁止指令重排序优化。
-
分析class字节码文件得知
- 有volatile修饰的变量,赋值后多执行了一个“lock addl $0x0,(%esp)”操作,这个操作相当于一个内存屏障
(Memory Barrier或Memory Fence,指重排序时不能把后 面的指令重排序到内存屏障之前的位置),
只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。
- lock前缀,查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU或者别的内核无效化(Invalidate)其Cache,
这种操作相当于对Cache中的变量做了一次前面介绍Java内存模式中所说的“store和write”操作。
所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存 中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入 主内存的变量中。
-
那为何说它禁止指令重排序呢?
- 从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。但并不是说指令任意重排,
CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。
- 譬如指令1把地 址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指 令2是有依赖的,它们之间的顺序
不能重排——(A+10)2与A2+10显然不相等,但指令3 可以重排到指令1、2之前或者中间,只要保证CPU执行后面依赖到A、B值的操作时能获取到 正确的A和B值即可。
所以在本内CPU中,重排序看起来依然是有序的。
- 因此,lock addl$0x0,(%esp)指令把修改同步到内存时,意味着所有之前的操作都已经执行完成, 这样便形成了“指令重排序无法越过内存屏障”的效果。