一个Java内存可见性问题的分析

如果熟悉Java并发编程的话,应该知道在多线程共享变量的情况下,存在“内存可见性问题”:

在一个线程中对某个变量进行赋值,然后在另外一个线程中读取该变量的值,读取到的可能仍然是以前的值;

这里并非说的是时序的问题,即使在另外一个线程中循环读取该变量的值,也可能永远读不到该变量的最新值。

请看下面这段代码:

 1 public class Main extends Thread {
 2     private static boolean flag = false;
 3     
 4     @Override
 5     public void run() {
 6         while (!flag) {
 7             //System.out.flush();
 8         }
 9     }
10     
11     public static void main(String[] args) {
12         Main m = new Main();
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应该关心

posted on 2016-07-30 12:06  frydsh  阅读(2249)  评论(0编辑  收藏  举报

导航