如果熟悉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. [WebServer] Tomcat 配置访问限制:访问白名单和访问黑名单

    前言: 昨天配置了 Tomcat 服务器运行 PHP 的环境,但是通过观察 Tomcat 这几天的日志发现,有很多莫名其妙的 IP 访问主机下莫名其妙的地址,如:/80./testproxy.php. ...

  2. 【DIY】【外壳】木板 & 亚克力 加工

    —————————————————————————————————————————————————————————————————————— 一.途径 淘宝 https://item.taobao.c ...

  3. 【图像处理】【SEED-VPM】5.uImage的烧写 & NFS烧写文件系统

    基于 TFTP 烧写 uImage 当用户对 SEED-VPM6467 下的内核驱动源码进行调整或者添加新的设备驱动后,需要对内核进行重新编译配置,编译生成内核镜像后,可以通过 tftp 下载到 SE ...

  4. mysql-5.6.17-win32的安装?

    官方mysql最新版本:http://cdn.mysql.com/Downloads/MySQL-5.6/mysql-5.6.17.tar.gz 解压后,以管理员的身份打开cmd.exe,切入安装目录 ...

  5. Ubuntu下Nutch1.2的使用

    aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAeMAAABpCAIAAACGSdxlAAAAA3NCSVQICAjb4U/gAAAgAElEQVR4Xu ...

  6. Bootstrap<基础四> 代码

    Bootstrap 允许您以两种方式显示代码: 第一种是 <code> 标签.如果您想要内联显示代码,那么您应该使用 <code> 标签. 第二种是 <pre> 标 ...

  7. C学习

    \a:警报 1.exit(),提前结束程序.include <stdlib.h> 2.getch()无缓存.getchar()有缓存,多条连用时注意末尾换行符否则始终.putchar(). ...

  8. 总结Themida / Winlicense加壳软件的脱壳方法

    总结下Themida/ Winlicense (TM /  WL) 的脱壳方法. 1, 查看壳版本,这个方法手动也可以,因为这个壳的版本号是写在程序里面的,在解压后下断点即可查看,这里有通用的脚本,我 ...

  9. js倒计时显示

    <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>j ...

  10. CSS之过渡简单应用—日落西山

    代码: <!DOCTYPE html><html><head> <title>日落西山</title> <meta charset=& ...