zoukankan      html  css  js  c++  java
  • 内存可见性,指令重排序,JIT。。。。。。从一个知乎问题谈起

    在知乎上看到一个问题《java中volatile关键字的疑惑?》,引起了我的兴趣

    问题是这样的:

     1 package com.cc.test.volatileTest;
     2 
     3 public class VolatileBarrierExample {
     4     private static boolean stop = false;
     5 
     6     public static void main(String[] args) throws InterruptedException {
     7         Thread thread = new Thread(new Runnable() {
     8             @Override
     9             public void run() {
    10                 while (!stop) {
    11                 }
    12             }
    13         });
    14 
    15         thread.start();
    16         Thread.sleep(1000);
    17         stop = true;
    18         thread.join();
    19     }
    20 }

    这段代码的主要目的是:主线程修改非volatile类型的全局变量stop,子线程轮询stop,如果stop发生变动,则程序退出。

    但是如果实际运行这段代码会造成死循环,程序无法正常退出。

    如果对Java并发编程有一定的基础,应该已经知道这个现象是由于stop变量不是volatile的,主线程对stop的修改不一定能被子线程看到而引起的。

    但是题主玩了个花样,额外定义了一个static类型的volatile变量i,在while循环中对i进行自增操作,代码如下所示:

     1 package com.cc.test.volatileTest;
     2 
     3 public class VolatileBarrierExample {
     4     private static boolean stop = false;
     5     private static volatile int i = 0;
     6 
     7     public static void main(String[] args) throws InterruptedException {
     8         Thread thread = new Thread(new Runnable() {
     9             @Override
    10             public void run() {
    11                 int i = 0;
    12                 while (!stop) {
    13                     i++;
    14                 }
    15             }
    16         });
    17 
    18         thread.start();
    19         Thread.sleep(1000);
    20         stop = true;
    21         thread.join();
    22     }
    23 }

    这段程序是可以在运行一秒后结束的,也就是说子线程对volatile类型变量i的读写,使非volatile类型变量stop的修改对于子线程是可见的!

    看起来令人感到困惑,但是实际上这个问题是不成立的。

    先给出概括性的答案:stop变量的可见性无论在哪种场景中都没有得到保证。这两个场景中程序是否能正常退出,跟JVM实现与CPU架构有关,没有确定性的答案。

    下面从两个不同的角度来分析

    一:happens-before原则:

    第一个场景就不谈了,即使在第二种场景里,虽然子线程中有对volatile类型变量i的读写+非volatile类型变量stop的读,但是主线程中只有对非volatile类型变量stop的写入,因此无法建立 (主线程对stop的写) happens-before于 (子线程对stop的读) 的关系

    也就是不能指望主线程对stop的写一定能被子线程看到。

    虽然场景二在实际运行时程序依然正确终止了,但是这个只能算是运气好,如果换一种JVM实现或者换一种CPU架构,可能场景二也会陷入死循环。

    可以设想这样的一个场景,主/子线程分别在core1/core2上运行,core1的cache中有stop的副本,core2的cache中有stop与i的副本,而且stop和i不在同一条cacheline里。

    core1修改了stop变量,但是由于stop不是volatile的,这个改动可以只发生在core1的cache里,而被修改的cacheline理论上可以永远不刷回内存,这样core2上的子线程就永远也看不到stop的变化了。

    二:JIT角度:

    由于run方法里的while循环会被执行很多次,所以必然会触发jit编译,下面来分析两种情况下jit编译后的结果(触发了多次jit编译,只贴出最后一次C2等级jit编译后的结果)

    如何查看JIT后的汇编码请参看我的这篇博文:《如何在windows平台下使用hsdis与jitwatch查看JIT后的汇编码》

    ps. 回答首发于知乎,重新截图太麻烦,因此实际分析使用的Java源码与前面贴的代码略有不同,不影响理解,会意即可。

    A. i为run方法内的局部变量的情况:

      1. 在第一个红框处检测stop变量,如果为true,那么跳转到L0001处继续执行(L0001处再往下走函数就退出了),但此时stop为false,所以不会走这个分支
      2. L0000,inc %ebp。也就是i++
      3. test %eax, -0x239864a(%rip),轮询SAFEPOINT的操作,可以无视
      4. jmp L0000,无条件跳转回L0000处继续执行i++

    如果把jit编译后的代码改写回来,大概是这个样子

    1 if(!stop){
    2      while(true){
    3           i++;
    4     }
    5 }

    非常明显的指令重排序,JVM觉得每次循环都去访问非volatile类型的stop变量太浪费了,就只在函数执行之初访问一次stop,后续无论stop变量怎么变,都不管了。

    第一种情况死循环就是这么来的。

    B. i为全局的volatile变量的情况:

     

    从第一个红框开始看:

      1. jmp L0001,无条件跳转到label L0001处
      2. movzbl 0x6c(%r10),%r8d; 访问static变量stop,并将其复制到寄存器r8d里
      3. test %r8d, %r8d; je L0000; 如果r8d里的值为0,跳转到L0000处,否则继续往下走(函数结束)
      4. L000: mov 0x68(%r10), %r8d; 访问static变量i,并将其复制到寄存器r8d里
      5. inc %r8d; 自增r8d里的值
      6. mov %r8d, 0x68(%r10); 将自增后r8d里的新值复制回static变量i中(上面三行是i++的流程)
      7. lock addl $0x0, (%rsp); 给rsp寄存器里的值加0,没有任何效果,关键在于前面的lock前缀,会导致cache line的刷新,从而实现变量i的volatile语义
      8. test %eax, -0x242a056(%rip); 轮询SAFEPOINT的操作,可以无视
      9. L0001,回到step 2

    也就是说,每次循环都会去访问一次stop变量,最终访问到stop被修改后的新值(但是不能确保在所有JVM与所有CPU架构上都一定能访问到),导致循环结束。

     这两种场景的区别主要在于第二种情况的循环中有对static volatile类型变量i的访问,导致jit编译时JVM无法做出激进的优化,是附加的效果。

    总结

    涉及到内存可见性的问题,一定要用happens-before原则细致分析。因为你很难知道JVM在背后悄悄做了什么奇怪的优化。

  • 相关阅读:
    免费的视频、音频转文本
    Errors are values
    Codebase Refactoring (with help from Go)
    Golang中的坑二
    Cleaner, more elegant, and wrong(msdn blog)
    Cleaner, more elegant, and wrong(翻译)
    Cleaner, more elegant, and harder to recognize(翻译)
    vue控制父子组件渲染顺序
    computed 和 watch 组合使用,监听数据全局数据状态
    webstorm破解方法
  • 原文地址:https://www.cnblogs.com/stevenczp/p/7978554.html
Copyright © 2011-2022 走看看