如果下面两段代码你能清楚的知道是为什么,那么就没有必要看本文了
代码示例1:下面的代码不会打印出“粮粮”
public class Act {static int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
Thread.sleep(1000);// 省略try catch
a = 1;
}).start();
while (a == 0) {
}
System.out.println("粮粮");
}
}
代码示例2:下面的代码会打印出“粮粮”
public class Act {static int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
Thread.sleep(1000);// 省略try catch
a = 1;
}).start();
while (a == 0) {
//同示例1相比,只多出了这一行代码
System.out.println();
}
System.out.println("粮粮");
}
}
本文对于C语言的朋友来说可能比较容易阅读,但是对于只会Java的朋友来说稍微有些复杂,这篇AT&T拓展内联汇编(ATT/GCC)会让你更好的理解本文,当然了,不看也可以。。。
Java中volatile关键字的的作用是每次都能获取最新的变量(主存)数据,很明显,虽然上述2个示例中我没有使用volatile关键字,但是在示例2中,也依然达到了volatile的效果,本文要论述的问题就是示例2的底层本质,虽然本文标题是volatile如何实现,但文章内容更倾向于内存屏障,这也包含了volatile的在hotspot中的实现原理,所以我将本文标题定义为volatile实现
Load与Store
Load:将内存中的数据放到寄存器
Store:将寄存器中的数据放到内存
由于JVM是基于栈的虚拟机,所以JVM会使用栈来模拟寄存器,在java中叫做操作数栈,用栈帧中的局部变量表来模拟物理机器的内存,所以我们也可以说
Load操作 :从一个对象的成员变量中读取值
Store操作:往一个对象的成员变量中写入值
下面的代码解释了什么是Load和Store
public class Demo1{int a;//成员变量
public void m1() {
int p = a;//Load a
a = 666; //Store a
}
}
编译器重排序
下面的示例方法m1中,对于编译器来说,先执行p1 = a,还是先执行p2 = b,在单线程下语义都是一样的,所以编译器编译出来的文件有可能先执行p2 = b,再执行p1 = a,这种现象就叫做编译器重排序
public class Demo1{
int a;//成员变量
int b;//成员变量
public void m1() {
int p1 = a;//Load a操作
int p2 = b;//Load b操作
}
}
CPU重排序
CPU为了流水线饱和,很有可能同编译器一样也会进行重排序的操作,也就是说,对于上述代码,即使编译器不重排序,CPU也有可能进行重排序
内存屏障
那么如何禁止编译器重排序呢?使用一个叫做内存屏障的东西,内存屏障就像一堵墙,屏障两边的代码可以互相排序,但是屏障一侧的代码,不能排序到另一侧,其实本应该分两种情况来说,1种是编译器屏障,在hotspot中使用AT&T语法的拓展内联汇编的volatile关键字来实现,另1种是内存屏障,在hotspot中的拓展内联汇编中加入lock前缀,这两种看不明白没关系,总之记住一句话,能防止两部分代码重排序的方式,就叫做内存屏障,hotspot抽象出了4种屏障,请看下面代码中的注释部分
int a;
int b;
public void m1() {
int p1 = a;//Load a操作
// 如果我在此处加一行代码,能导致先执行a,再执行b,那么这行代码就叫做内存屏障
int p2 = b;//Load b操作
}
}
LoadLoad屏障
上述代码如果在p1 = a和p2 = b之间加点东西,那么此时这个东西就叫做内存屏障,由于p1 = a和p2 = b都是Load操作,所以这种屏障就叫做LoadLoad屏障,下面的代码是稍微修改之后的代码
int a;
int b;
public void m1() {
int p1 = a;//Load a操作
如果在此行插入屏障,则该屏障叫做LoadLoad屏障
int p2 = b;//Load b操作
}
}
如果想要禁止重排序,得加上一个LoadLoad屏障,那么具体如何加呢??在Java语言的层次上,是Unsafe类的fullFence,当然了,程序员无法直接使用该类,而且该类在java9中被其他类替代了,具体是什么我也没研究,总之,能明白这个意思就行,因为对于上述示例来说,真的没有必要禁止重排序,内存屏障主要的使用者是编译器,而不是程序员,后文会给出解释和示例
LoadStore屏障
同理,除了LoadLoad,还有LoadStore,StoreStore,StoreLoad屏障,下面的代码如果想要禁止重排序,则需要添加LoadStore屏障
int a;
int b;
public void m1() {
int p1 = a;//Load a操作
如果在此行插入屏障,则该屏障叫做LoadStore屏障
b=666; //Store b操作
}
}
使用屏障的场景
到目前为止,上文虽然表达了什么是屏障,但是只是为了更好的解释什么是内存屏障,并不是真正的使用场景,因为上述代码即使不使用屏障也没有丝毫问题,那么在java中,到底满足哪些条件,才会使用屏障呢?在hotspot中,如果两个挨着的操作,满足如下关系,则会被自动插入内存屏障
根据上表规则可知,如下代码在编译时必定会被插入内存屏障
public class Demo{volatile int v1;
int a;
public void m1() {
int p1 = v1;//Load v1,这个操作也叫读取volatile成员v1
这里会被插入LoadStore内存屏障
a=666; //Store a操作,这个操作也叫写入普通成员a
}
}
到此为止,这也真正说明了为什么会说volatile有禁止重排序的效果,其实它的本质是编译器根据自己的规则,而这个规则波及到了被volatile修饰的成员变量,所以有人说volatile会禁止重排序,通过上述规则,那么我们可以大胆的推测出下面的代码也会被插入内存屏障
public class Demo{volatile int v1;
int a;
public void m1() {
synchronized (this) {// 进入synchronized(线程进入监视区)
这里会被插入LoadStore内存屏障
a = 666; // Store a操作,也叫写入普通成员a
} // 退出synchronized(线程退出监视区)
这里会被插入StoreLoad内存屏障
a = v1; // Load v1操作,也叫读取volatile成员v1
}
}
内存屏障如何实现的?
4中内存屏障,LoadLoad,LoadStore,StoreStore,StoreLoad的实现方式是相同的,它们的源代码是GCC的AT&T内联汇编写的,定义如下
没错,如你所想,这行代码就是内存屏障,重点在于它不仅有内存屏障的功能,它还有清空寄存器的语义,"memory"表示每次读取都从主存(而非寄存器)读取数据,所以有内存屏障的地方,读取的数据都是最新的数据,回想本文开头的代码示例1和代码示例2,发现示例2会有volatile的效果,最终答案就是System.out.println方法里面有synchronized块,所以会出现内存屏障,而出现内存屏障,就会从主存读取数据,从而达到获取最新值的效果