如果熟悉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. 绑定多个ddl

    添加材料,需要绑定材料类型.设备名称.省份和所属终端客户等信息,前台页面如下: 前台.aspx <asp:Content ID="Content2" ContentPlace ...

  2. Fortran版MPI_barrier出现错误可能情况

    在Fortran中的MPI_开头函数都常有一个整数型的错误变量用以函数返回错误信息.如: call MPI_Barrier(MPI_COMM_WORLD,ierr) 在没有ierr参数时,程序可能会出 ...

  3. Man——send(2)翻译

    ##纯手打 Man——send(2) -->NAME: send, sendto, sendmsg - 在socket上发送一条消息 -->总览: #include <sys/typ ...

  4. java mysql 日期类型

    mysql(版本:5.1.50)的时间日期类型如下: datetime 8bytes xxxx-xx-xx xx:xx:xx 1000-01-01 00:00:00到9999-12-31 23:59: ...

  5. Python时区转换

    最近工作中遇到了一个问题:我的server和client不是在一个时区,server时区是EDT,即美国东部时区,client,就是我自己的电脑,时区是中国标准时区,东八区.处于测试需要,我需要向se ...

  6. 利用HttpURLConnection发送请求

    HttpURLConnection: 每个 HttpURLConnection实例都可用于生成单个请求,但是其他实例可以透明地共享连接到 HTTP 服务器的基础网络.请求后在 HttpURLConne ...

  7. nginx rewrite

    # nginx rewrite rule #Main menu rules.rewrite ^(.*?)/index.html$ $1/index.php?r=site/index break;rew ...

  8. Ubuntu下freeradius的EAP-MD5,PEAPv0/EAP-MSCHAPv2,EAP-TTLS/MD5,EAP-TTLS/MSCHAPv2方式认证(基于mysql)

    基于freeradius+mysql,今天验证下freeradius的EAP认证:1.EAP-MD5:2.EAP-PEAP 一.EAP-MD5方式认证 1.修改配置文件 (1)/usr/local/e ...

  9. auto_ptr源码剖析

    /* * Copyright (c) 1997-1999 * Silicon Graphics Computer Systems, Inc. * * Permission to use, copy, ...

  10. Jquery实现select左右栏的添加移除

    首先是效果展示, 兼容火狐,IE6+,谷歌没测试有