zoukankan      html  css  js  c++  java
  • 一个Java内存可见性问题的分析

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

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

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

    请看下面这段代码:

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

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

    更诡异的是,如果将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应该关心

  • 相关阅读:
    React项目升级遇到的问题复盘(2019-09-02)
    前端项目升级到React-router5中遇到的问题解决方案以及思路
    三行Jquery代码实现简单的选项卡
    开放-封闭原则
    单一职责原则
    简单工厂模式(c++实现)
    博客园使用MarkDown格式记录博客
    Qml 的Image对应的source不变,但是图片内容改变却不会刷新的解决方案
    Qt中第一请求web api连接返回缓慢问题
    Qt的pro文件工程配置
  • 原文地址:https://www.cnblogs.com/frydsh/p/5720658.html
Copyright © 2011-2022 走看看