volatile 有两个作用
1 线程内存可见性
2 指令排序
可见性原理:
线程在对Volatile变量执行写操作时强迫线程将最新的值刷新到主内存中,而在读操作时强迫从主内存中读入变量的值
2 指令重排
使用了volatile修饰的变量,在对改变量进行读写的时候会添加屏障规则;
在讲指令重排之前先介绍内存屏障
内存屏障(memory barrier)是一个CPU指令。这条指令可以确保一些特定指令的执行顺序,相当于在上下两条指令之间添加一个障碍,阻止指令交换位置执行。
jvm提供了四种屏障规则:Load=>读,Store=>写
1)LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
(2)StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
(3)LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
(4)StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
举个例子:
public class Test2 { T o = new T(); } class T { int m = 2; }
查看汇编代码
主要关注5 9 12 三行代码
5=》在内存中开辟一个空间用来给o对象使用,但是此时都是初始值,m=0;
9=>调用T的构造方法,m=2;
12=>讲o指向刚开辟的内存空间的地址
此时如果进行指令重排很可能是先执行12 然后在执行9。这就是为什么懒加载双重锁创建单例对象时 为什么要用volatile修饰了来禁止指令重排。
使用volatile修饰之后
public class Test2 { volatile T o = new T(); } class T { int m = 2; }
相当于在12行上线加了StoreStore StoreRead指令,从而不会让12和9调换位置