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

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

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

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

请看下面这段代码:多线程

 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)的行为更保守一些而已。并发

疑惑在于,为何是“永远不可见”?我以前一直觉得“内存可见性问题”只是时间长短而已。ide

更诡异的是,若是将while循环中的System.out.flush()打开的话,程序又均可以正常结束了,这又是什么缘由呢?post

首先,咱们从字节码入手,发现它们对应的字节码基本上是同样的;即便是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应该关心

相关文章
相关标签/搜索