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

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

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

请看下面这段代码:

 public class Main extends Thread {
private static boolean flag = false; @Override
public void run() {
while (!flag) {
//System.out.flush();
}
} public static void main(String[] args) {
Main m = new Main();
m.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
try {
m.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("done");
}
}

这段代码在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应该关心

一个Java内存可见性问题的分析的更多相关文章

  1. 从一个小例子引发的Java内存可见性的简单思考和猜想以及DCL单例模式中的volatile的核心作用

    环境 OS Win10 CPU 4核8线程 IDE IntelliJ IDEA 2019.3 JDK 1.8 -server模式 场景 最初的代码 一个线程A根据flag的值执行死循环,另一个线程B只 ...

  2. Java内存模型JMM简单分析

    参考博文:http://blog.csdn.net/suifeng3051/article/details/52611310 http://www.cnblogs.com/nexiyi/p/java_ ...

  3. Java内存可见性

    如果一个线程对共享变量的修改,能够被其它线程看到,那么就能说明共享变量在线程之间是可见的.如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量.Java内存模型(Java ...

  4. Java内存可见性volatile

    概述 JMM规范指出,每一个线程都有自己的工作内存(working memory),当变量的值发生变化时,先更新自己的工作内存,然后再拷贝到主存(main memory),这样其他线程就能读取到更新后 ...

  5. java 内存可见性

    java线程 -> 线程工作内存 -> 主物理内存 线程工作内存的原理是栈内是连续的小空间,寻址速度比堆快得多,将变量拷贝到栈内生成副本再操作 什么是重排序 代码指令可能并不是严格按照代码 ...

  6. 一个java内存泄漏的排查案例

    这是个比较典型的java内存使用问题,定位过程也比较直接,但对新人还是有点参考价值的,所以就纪录了一下. 下面介绍一下在不了解系统代码的情况下,如何一步步分析和定位到具体代码的排查过程 (以便新人参考 ...

  7. java内存问题排查及分析

    最近了解了一下jdk对于jvm分析工具的使用,下面通过一个简单的列子介绍一下,以下内容部分来自其他帖子. 下面这段代码明显有问题(从网上抄的) import java.util.HashMap; im ...

  8. CognitiveJ一个Java的人脸图像识别开源分析库

    CognitiveJ 是一个开源的,支持 Java 8 API 的库,用于管理和编排 Java 应用和微软的Cognitive(Project Oxford)机器学习和图像处理库的项目,可以让你查询以 ...

  9. java 内存 线程 类 vm分析工具

    JMeter.Jconsole.JVMStat

随机推荐

  1. [css]水平垂直居中的方法

    1.top:cale(50% - 2rem); left:cale(50% - 2rem);

  2. linux学习 命令ll后字段的解释(转)

    原文链接:http://jz20080153.blog.sohu.com/161554070.html ls -l 列表信息详解 我们平时用ls -l 命令查看一个目录下的文件和子目录的详悉信息时,会 ...

  3. gridview的rowdeleting这个函数总是不执行

    今天在做新闻管理时,管理数据的时候需要弹出确认删除的功能,可是此功能总是不能够实现,调试的时候也执行不到该方法,后来方向是忘记给button加上一个属性: 把CommandName设置为delete. ...

  4. java培训第一天--画板

    package day1; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt ...

  5. UDPClient的用法

    UDP_Server: UdpClient receivingUdpClient = ); IPEndPoint RemoteIpEndPoint = ); try { byte[] sdata = ...

  6. alert弹层无法取消问题解决办法

    最近做H5移动端开发的时候,js代码写了个alert,在Android手机上能正常运行,但是在IOS上运行弹出之后却无法取消掉, 而且页面卡死,点不了任何东西,这种情况是非常不好的,用户体验非常糟糕. ...

  7. JAVA 中文转GBK内码方法

    不能谷歌,百度了很久,没有直接的转换方法,参考 byte[]数组与十六进制字符串与字符串的互相转换 http://blog.163.com/roadwalker@126/blog/static/113 ...

  8. LLDB, iOS调试器

    breakpoint set -s "" breakpoint set    -M <method> ( --method <method> )    -S ...

  9. Android深度探索(卷1)HAL与驱动开发

    第一章 介绍Android驱动开发和移植技术 主要对android和linux做了总体的介绍,让我们有了个感性的认识. 一.Android的四层系统架构: a) Linux内核:Android是基于L ...

  10. js报错:email() is not a function

    email() is not a function 明明是一个函数,但火狐控制台真J.. 由于JSP文件是别人写好直接使用的,所以,来回测试,折腾!最后,没办法,一段一段代码删除测试,才发现.有for ...