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

如果熟悉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行为?):

b9a2c14d02a0abeca036a61bf7f67dff.png

这个是无volatile无System.out.flush()的版本,它不能停止,说明如下:

第一个红色标记,读取flag的值

第二个红色标记,判断flag的值是否为false,如果是则顺序执行到第三个红色标记处

第三个红色标记,这里是一个死循环

从这里可以看出,JIT对生成的代码做了高度优化,它认为代码中没有地方对flag进行修改,因此直接生成一段死循环代码,避免反复读取flag的值以提升性能,但是这违背了这段代码的原意,导致程序不能停止。

6edfbf854317aaceb4b9b7022fb701b3.png

这个是有volatile的版本,它可以正常结束,说明如下:

第一个红色标记,读取flag的值

第二个红色标记,判断flag的值是否为false,如果是则跳转到第个红色标记处

这完全符合这段代码的原意,因此可以正常结束。

e2dfd70887d340bca1287c8936943b84.png

这个是有System.out.flush()的版本,从红色标记处可以看出,这里也完全符合代码原意,因此可以正常结束;由于某种原因,JIT没有对生成的代码进行优化。

至此,疑惑已完全解开,在此也顺便总结一下Java中的volatile关键字:

1.阻止Java编译器对字节码进行重排序(似乎没有Java实现在字节码层面进行重排序)

2.在JIT生成的代码中插入适当的内存屏障指令

3.禁止JIT过度优化生成的代码

3.字节码层面并不会关心volatile(变量标记除外),执行引擎和JIT应该关心

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值