如果熟悉Java并发编程的话,应该知道在多线程共享变量的情况下,存在“内存可见性问题”:
在一个线程中对某个变量进行赋值,然后在另外一个线程中读取该变量的值,读取到的可能仍然是以前的值;
这里并非说的是时序的问题,即使在另外一个线程中循环读取该变量的值,也可能永远读不到该变量的最新值。
请看下面这段代码:
1 public class Main extendsThread {2 private static boolean flag = false;3
4 @Override5 public voidrun() {6 while (!flag) {7 //System.out.flush();
8 }9 }10
11 public static voidmain(String[] args) {12 Main m = newMain();13 m.start();14 try{15 Thread.sleep(200);16 } catch(InterruptedException e) {17 e.printStackTrace();18 }19 flag = true;20 try{21 m.join();22 } catch(InterruptedException e) {23 e.printStackTrace();24 }25 System.out.println("done");26 }27 }
这段代码在Windows(Java 7 HotSpot),Linux(Java 7 OpenJDK),MacOS(Java 7 HotSpot)上运行的时候根本停不下开;然而在Android(Dalvik)上,类似的代码则可以正常结束;我们知道,如果将变量flag声明为volatile的话,那么这段代码不管在哪个平台上运行都可以正常结束,事实也确实如此;这些平台都没有问题,它们的行为都符合JMM规范,只不过Android(Dalvik)的行为更保守一些而已。
疑惑在于,为什么是“永远不可见”?我之前一直以为“内存可见性问题”只是时间长短而已。
更诡异的是,如果将while循环中的System.out.flush()打开的话,程序又都可以正常结束了,这又是什么原因呢?
首先,我们从字节码入手,发现它们对应的字节码基本上是一样的;即使是volatile版本,也只不过是在变量上增加了一个volatile标记,字节码并无不同。
据此,我们可以推断,差异可能来源于JIT,于是关掉JIT(如何控制JVM中的JIT行为?),果然,这些代码又都可以正常结束了。
按照我之前学习到的一些有关多核CPU方面的知识,多核CPU的行为并不会导致“永远不可见”的问题,理由如下:
1.如果是CPU缓存,多核CPU之间存在“缓存一致性”协议,所以这里并不会导致“不可见”的问题;
2.如果是CPU Store Buffer,因为容量有限,迟早会写回到缓存,所以这里并不会导致“永远不可见”的问题;
3.如果是CPU指令重排序,由于这段代码是在一个循环中读取变量的值,所以这里不会有任何影响。
那么,问题就只能出在JIT生成的代码上了,让我们查看一下JIT生成的代码(如何控制JVM中的JIT行为?):
这个是无volatile无System.out.flush()的版本,它不能停止,说明如下:
第一个红色标记,读取flag的值
第二个红色标记,判断flag的值是否为false,如果是则顺序执行到第三个红色标记处
第三个红色标记,这里是一个死循环
从这里可以看出,JIT对生成的代码做了高度优化,它认为代码中没有地方对flag进行修改,因此直接生成一段死循环代码,避免反复读取flag的值以提升性能,但是这违背了这段代码的原意,导致程序不能停止。
这个是有volatile的版本,它可以正常结束,说明如下:
第一个红色标记,读取flag的值
第二个红色标记,判断flag的值是否为false,如果是则跳转到第个红色标记处
这完全符合这段代码的原意,因此可以正常结束。
这个是有System.out.flush()的版本,从红色标记处可以看出,这里也完全符合代码原意,因此可以正常结束;由于某种原因,JIT没有对生成的代码进行优化。
至此,疑惑已完全解开,在此也顺便总结一下Java中的volatile关键字:
1.阻止Java编译器对字节码进行重排序(似乎没有Java实现在字节码层面进行重排序)
2.在JIT生成的代码中插入适当的内存屏障指令
3.禁止JIT过度优化生成的代码
3.字节码层面并不会关心volatile(变量标记除外),执行引擎和JIT应该关心