资讯专栏INFORMATION COLUMN

从汇编看Volatile的内存屏障

szysky / 2977人阅读

摘要:为了实现的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。上述写和读的内存屏障插入策略非常保守。

本讲座地址https://segmentfault.com/l/15... 欢迎大家围观

Java的Volatile的特征是任何读都能读到最新值,本质上是JVM通过内存屏障来实现的,让我们看看从字节码以及汇编码的角度,来看下是否真是如此?

一 Volatile与内存屏障

本节内容来自:http://www.infoq.com/cn/artic...

为了实现volatile内存语义,JMM会分别限制重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:

举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

从上表我们可以看出:

当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:

上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。

这里比较有意思的是volatile写后面的StoreLoad屏障。这个屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里我们可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。

下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:

上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面我们通过具体的示例代码来说明:

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           //第一个volatile读
        int j = v2;           // 第二个volatile读
        a = i + j;            //普通写
        v1 = i + 1;          // 第一个volatile写
        v2 = j * 2;          //第二个 volatile写
    }

    …                    //其他方法
}

针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。

上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

前面保守策略下的volatile读和写,在 x86处理器平台可以优化成:

前文提到过,x86处理器仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。

二 Volatile的字节码

为了搞清楚内存屏障,我们扒开class字节码看一下,用javap -v -p class文件名(不要.class 后缀)运行

volatile int v1;
    descriptor: I
    flags: ACC_VOLATILE
   .....
void readAndWrite();
    descriptor: ()V
    flags:
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: getfield      #52                 // Field v1:I
         4: istore_1
         5: aload_0
         6: getfield      #54                 // Field v2:I
         9: istore_2
        10: aload_0
        11: iload_1
        12: iload_2
        13: iadd
        14: putfield      #72                 // Field a:I
        17: aload_0
        18: iload_1
        19: iconst_1
        20: isub
        21: putfield      #52                 // Field v1:I
        24: aload_0
        25: iload_2
        26: iload_1
        27: imul
        28: putfield      #54                 // Field v2:I
        31: return

除了其变量定义的时候有一个Volatile外,之后的字节码跟有无Volatile完全一样,于是我们又扒了下汇编代码

三 Volatile的汇编码

为了看到汇编码,要使用hsdis插件, 在mac系统下需要安装一个hsdis-amd64.dylib的插件。在网上找了一个,地址在这里。
下载下来后,将其放置到你的jre lib目录下即可。
mac系统上命令如下,

sudo mv ./hsdis-amd64.dylib  /Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/lib

然后再运行

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*VolatileBarrierExample.readAndWrite -XX:CompileCommand=compileonly,*VolatileBarrierExample.readAndWrite com.earnfish.VolatileBarrierExample > out.put

其中*VolatileBarrierExample.readAndWrite表示你运行的类.函数, com.earnfish.VolatileBarrierExample表示你的包名.类名,注意需要有main函数来运行你所要执行的函数。得出汇编码如下

  0x000000011214bb49: mov    %rdi,%rax
  0x000000011214bb4c: dec    %eax
  0x000000011214bb4e: mov    %eax,0x10(%rsi)
  0x000000011214bb51: lock addl $0x0,(%rsp)     ;*putfield v1
                                                ; - com.earnfish.VolatileBarrierExample::readAndWrite@21 (line 35)

  0x000000011214bb56: imul   %edi,%ebx
  0x000000011214bb59: mov    %ebx,0x14(%rsi)
  0x000000011214bb5c: lock addl $0x0,(%rsp)     ;*putfield v2
                                                ; - com.earnfish.VolatileBarrierExample::readAndWrite@28 (line 36)

其对应的Java代码如下

 v1 = i - 1;          // 第一个volatile写
 v2 = j * i;          // 第二个volatile写

可见其本质是通过一个lock指令来实现的。那么lock是什么意思呢?

查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。

所以,它的作用是

锁住主存

任何读必须在写完成之后再执行

使其它线程这个值的栈缓存失效

类似于前面是storestore,后面是storeload

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69025.html

相关文章

  • volatile,可见性,有序性

    摘要:内存语义的的实现可见性的实现基于的读取,写入两个操作的内存语义。首先,对中内存屏障的介绍内存屏障用于控制特定条件下的重排序和内存可见性问题。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 volatile,可见性,有序性 volatile的特性 可见性:对一个volatile变量的读,总能获取其他任意线程对该变量最后的写入。 有序性:JMM会限制volat...

    caige 评论0 收藏0
  • (七)Volatile作用及原理

    摘要:文章简介分析的作用以及底层实现原理,这也是大公司喜欢问的问题内容导航的作用什么是可见性源码分析的作用在多线程中,和都起到非常重要的作用,是通过加锁来实现线程的安全性。而的主要作用是在多处理器开发中保证共享变量对于多线程的可见性。 文章简介 分析volatile的作用以及底层实现原理,这也是大公司喜欢问的问题 内容导航 volatile的作用 什么是可见性 volatile源码分析 ...

    marek 评论0 收藏0
  • volatile详解

    摘要:内存模型基本概念计算机在执行程序时,每条指令都是在中执行的,而执行指令过程中,势必涉及到数据的读取和写入。有序性即程序执行的顺序按照代码的先后顺序执行。 内存模型基本概念 计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据...

    aikin 评论0 收藏0
  • Java并发编程,3分分钟深入分析volatile实现原理

    摘要:一言以蔽之,被修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。为了实现内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。volatile原理volatile简介Java内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。 线程在工作内存进行操作后何时会写到主内存中...

    番茄西红柿 评论0 收藏0
  • Java并发编程,3分分钟深入分析volatile实现原理

    摘要:一言以蔽之,被修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。为了实现内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。volatile原理volatile简介Java内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。 线程在工作内存进行操作后何时会写到主内存中...

    番茄西红柿 评论0 收藏0

发表评论

0条评论

szysky

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<