Volatile从入门到放弃

本文详细探讨了Java中volatile的内存语义和实现机制,包括编译器乱序、CPU乱序及其对程序的影响。文章阐述了编译器屏障与内存屏障的作用,解释了在JVM中volatile的实现细节,以及volatile如何确保可见性和防止重排序。同时,文章通过源码分析揭示了Java volatile的读写操作与内存屏障的关系,并讨论了其与多线程原子性操作的区别。
摘要由CSDN通过智能技术生成

1.引言

        如果你对Java的volatile有着疑虑请阅读本文,如果你想对volatile想有一个更深的了解,请阅读本文.本文主要讲的是volatile的写happen-before在volatile读之前所涉及相关的原理,以及在Hotspot中相关代码的实现.        

首先从一段代码开始讲起,如下

    初始化

  1. int a = 0, int b = 0;  
  2.   void foo(void)  
  3.  {  
  4.        a= 1;  
  5.        b= 1;  
  6.  }  
  7.  void bar(void)  
  8.  {  
  9.    while (b == 0) continue;  
  10.     If(a == 1) {  
  11.      System.out.println(“true”);  
  12.     };  
  13.   }  
 int a = 0, int b = 0;
   void foo(void)
  {
        a= 1;
        b= 1;
  }
  void bar(void)
  {
    while (b == 0) continue;
     If(a == 1) {
      System.out.println(“true”);
     };
   }

以上的代码,threadA运行foo方法,threadB运行bar方法;在threadB中,能否在while循环跳出之后,即b=1的情况下,得到a一定等于1呢?答案是否定的.因为存在着编译器的乱序和cpu指令的乱序,程序没有按照我们所写的顺序执行.

 

2.1编译器乱序

编译器是如何乱序的?编译器在不改变单线程语义的前提之下,为了提高程序的运行速度,可以对指令进行乱序.

编译器按照一个原则,进行乱序:不能改变单线程程序的行为.

    在不改变单线程运行的结果,以上foo函数就有可能有两种编译结果:

第一种:

        a = 1;

        b= 1;

第二种:

       b = 1;

       a = 1;

如上,如果cpu按照第二种编译结果执行,那么就不能正确的输出”true”.虽然乱序了,但是并没有改变单线程threadA执行foo的语义,所以上面的重排是允许的.

 2.2.cpu乱序

2.2.1 cpu的结构与cpu乱序:

首先来了解一下x86 cpu的结构:

c1,c2,c3 .. cn是160个用于整数和144个用于浮点的寄存器单元,用于存储本地变量和函数参数.cpu访问寄存器的需要1cycle< 1ns,cpu访问寄存器是最快的.

LoadBuffer,storeBuffer合称排序缓冲(Memoryordering Buffers (MOB)),Load缓冲有64长度,store缓冲有36长度.buffer与L1进行数据传输,cpu无须等待.

L1是本地核心内的缓存,被分成独立的32K的数据缓存和32k指令缓存.访问L1需要3cycles ~1ns.

L2缓存是本地核心内的缓存,被设计为L1缓存与共享的L3缓存之间的缓冲,L2缓存大小为256K.访问L2需要12cycles-3ns.

L3:在同插槽的所有核心共享L3缓存,L3缓存被分为数个2M的段,访问L3需要38cycles~12ns.

DRAM:就是我们通常说的内存,访问内存一般需要65ns.

cpu加载不同缓存级别的数据,从上往下,需要的时间是越来越多的,而等待对cpu来说是极大的浪费;对于不同的插槽的cpu所拥有的一级二级缓存是不共享的,而不同的缓存之间需要保证一致性,主要通过MESI协议,为了保证不同缓存的一致性也是需要付出时间的,因为不同状态之间的转换需要等待消息的回复,这个等待过程往往是阻塞的.

根据MESI协议,变量要从S状态变为M状态,本cpu要执行read andmodify ,需要发出Invalidate消息,然后等待,待收到所有其它cpu的回复之后才执行更新状态,其他操作也是如此,都有会等待的过程。这些过程都是阻塞的动作,无疑会给cpu的性能带来巨大的损耗。

         经过不断地改进,在寄存器与cache之间加上loadbuffer、storebuffer,来减小这些阻塞的时间。cpu要读取数据时,先把读请求发到loadbuffer中,无需等待其他cpu的响应,就可以先进行下一步操作。直到其他cpu的响应结果达到之后,再处理这个读请求的结果。cpu写数据时,就把数据写到storebuffer中,此时cpu认为已经把数据写出去了,待到某个适合的时间点(cpu空闲时间),在把storebuffer的数据刷到主存中去。

         根据以上的描述,可知在storebuffer中的数据有临时可见性问题。即在storebuffer中的数据对本cpu是可见的(本cpu读取数据时,可以直接从storebuffer中进行读取),其它槽的cpu对storebuffer中的数据不可见。loadbuffer中的请求无法取到其他cpu修改的最新数据,因为最新数据在其他cpu的storebuffer中。同时,在loadbuffer、storebuffer中的"请求"的完成都是异步的,即表现为它们完成的顺序是不确定的,指令的乱序。

2.2.2  cpu乱序

cpu乱序的一种情况:

Processor 0

Processor 1

mov [ _x], 1

mov [ _y], 1

mov r1, [ _y]

mov r2, [_x]

Initially x == y == 0

 

解释一下:x,y表示内存中的变量,r1,r2表示寄存器, mov [_x],1,表示把1赋值给x变量,从顺序执行的角度来说,结果肯定是r1=1,或r2 =1;然而r1=0并且r2=0是存在的.

cpu实际执行的情况可能如下

Processor 0

Processor 1

mov r1, [ _y] //  (1)

 

 

mov r2, [_x] // (2)

mov [ _x], 1 //  (3)

 

 

mov [ _y], 1 // (4)

如上(1),(2),(3),(4)的顺序执行,便会得到r1=0,r2=0.可以这么理解,大部分的处理器,在保证单线程执行语义正确的基础上,会根据一定的规则对指令进行重排.  

为什么会出现上面的情况?

因为cpu会将写放入storebuffer中,然后立刻返回不会等待,会将读放入load buffer中,然后立刻返回,这两个队列都是异步的,他们写读的是不同地址,不存在依赖,两者谁先完成是不知道的,如果读指令先完成了,就出现上面的重排的情况.

3. 编译器屏障与内存屏障

由于存在着编译器对程序指令的重排,这个需要编译器屏障来保证程序的正确执行,cpu也会对指令进行重排,这个需要内存屏障来保证.所以屏障需要对编译器和内存同时起作用,来保证程序可以正确执行.

因为我们大部分的软件主要都是在x86_linux上运行的,所以我们主要研究的是x86的相关编译器屏障和内存屏障.

3.1 编译屏障

Open jdk9中x86_linux的栅栏指令编写如下:

static inline void compiler_barrier() {

 __asm__ volatile ("" : : : "memory"); /*编译器屏障*/

}

此处  __asm__ volatile("" : : : "memory"); 是内嵌汇编.

解释:

__asm__ :代表汇编代码开始.

__volatile__:禁止编译器对代码进行某些优化.

memory: memory代表是内存;这边用”memory”,来通知编译器内存的内容已经发生了修改,要重新生成加载指令(不可以从缓存寄存器中取).因为存在着内存的改变,不可以将前后的代码进行乱序.

asm volatile("" :::"memory"),这句内嵌汇编作为编译器屏障,可以防止编译器对相邻指令进行乱序,但是它无法阻止CPU的乱序;也就是说它仅仅禁止了编译器的乱序优化,不会阻止CPU的乱序执行。

以下利用asm volatile("" ::: "memory")来验证,对于屏障之后的读,都是读取的主存,而不是从寄存器中读取的.当然如果您不懂汇编和c++这部分您可以跳过.

实验一:

  1. #include <stdio.h>  
  2. int foo = 10;  
  3. int bar = 15;  
  4. int main(void)  
  5. {  
  6.        int  ss = foo + 1;  
  7.        __asm__ __volatile__("":::"memory");  
  8.        int foo1 = foo + 2;  
  9.        printf("ss=%d\n", ss);  
  10.        printf("foo1=%d\n", foo1);  
  11.          return0;  
  12. }  
#include <stdio.h>
int foo = 10;
int bar = 15;
int main(void)
{
       int  ss = foo + 1;
       __asm__ __volatile__("":::"memory");
       int foo1 = foo + 2;
       printf("ss=%d\n", ss);
       printf("foo1=%d\n", foo1);
         return0;
}


编译命令:

g++ -S -O2 test1.cpp

编译结果的部分汇编:

  1. main:  
  2. .LFB30:  
  3.        .cfi_startproc  
  4.        movl    foo(%rip), %eax  //将foo变量从内存加载到寄存器eax中  
  5.        pushq   %rbx  
  6.        .cfi_def_cfa_offset 16  
  7.        .cfi_offset 3, -16  
  8.        leal    1(%rax), %edx //将rax变量加1赋值给edx寄存器也是ss变量.                                                                          //rax是64位寄存器,它的低32位是eax寄存器.  
  9.        movl    foo(%rip), %eax//将foo变量从内存加载到寄存器eax中  
  10.        movl    $.LC0, %esi  
  11.        movl    $1, %edi  
  12.        leal    2(%rax), %ebx//将rax变量加2赋值给edx寄存器也是foo1变量.      
  13.        xorl    %eax, %eax  
  14.        call    __printf_chk  
  15.         movl   %ebx, %edx  
  16.        movl    $.LC1, %esi  
  17.        movl    $1, %edi  
  18.        xorl    %eax, %eax  
  19.        call    __printf_chk  
  20.        xorl    %eax, %eax  
  21.        popq    %rbx  
  22.        .cfi_def_cfa_offset 8  
  23.        ret  
  24.        .cfi_endproc  
main:
.LFB30:
       .cfi_startproc
       movl    foo(%rip), %eax  //将foo变量从内存加载到寄存器eax中
       pushq   %rbx
       .cfi_def_cfa_offset 16
       .cfi_offset 3, -16
       leal    1(%rax), %edx //将rax变量加1赋值给edx寄存器也是ss变量.                                                                          //rax是64位寄存器,它的低32位是eax寄存器.
       movl    foo(%rip), %eax//将foo变量从内存加载到寄存器eax中
       movl    $.LC0, %esi
       movl    $1, %edi
       leal    2(%rax), %ebx//将rax变量加2赋值给edx寄存器也是foo1变量.    
       xorl    %eax, %eax
       call    __printf_chk
        movl   %ebx, %edx
       movl    $.LC1, %esi
       movl    $1, %edi
       xorl    %eax, %eax
       call    __printf_chk
       xorl    %eax, %eax
       popq    %rbx
       .cfi_def_cfa_offset 8
       ret
       .cfi_endproc


实验二:

  1. #include <stdio.h>  
  2. int foo = 10;  
  3. int bar = 15;  
  4. int main(void)  
  5. {  
  6.        int  ss = foo + 1;  
  7.        int foo1 = foo + 2;  
  8.        printf("ss=%d\n", ss);  
  9.        printf("foo1=%d\n", foo1);  
  10.          return0;  
  11. }  
#include <stdio.h>
int foo = 10;
int bar = 15;
int main(void)
{
       int  ss = foo + 1;
       int foo1 = foo + 2;
       printf("ss=%d\n", ss);
       printf("foo1=%d\n", foo1);
         return0;
}


编译命令:

g++ -S -O2 test2.cpp

 

编译结果的部分汇编:

 

  1. main:  
  2. FB30:  
  3.      .cfi_startproc  
  4.      pushq   %rbx  
  5.      .cfi_def_cfa_offset 16  
  6.      .cfi_offset 3, -16  
  7.      movl    foo(%rip), %ebx //将foo变量从内存加载到寄存器ebx中  
  8.   
  9.      movl    $.LC0, %esi  
  10.      movl    $1, %edi  
  11.      xorl    %eax, %eax  
  12.      leal    1(%rbx), %edx//将rbx寄存器加1赋值给edx寄存器也是ss变量.                                                                      //rbx是64位寄存器,它的低32位是ebx寄存器.  
  13.      call    __printf_chk  
  14.      leal    2(%rbx), %edx//直接取rbx寄存器,加2赋值给foo1变量  
  15.      movl    $.LC1, %esi  
  16.      movl    $1, %edi  
  17.      xorl    %eax, %eax  
  18.       call   __printf_chk  
  19.      xorl    %eax, %eax  
  20.      popq    %rbx  
  21.      .cfi_def_cfa_offset 8  
  22.      ret  
  23.      .cfi_endproc  
  main:
.LFB30:
       .cfi_startproc
       pushq   %rbx
       .cfi_def_cfa_offset 16
       .cfi_offset 3, -16
       movl    foo(%rip), %ebx //将foo变量从内存加载到寄存器ebx中
 
       movl    $.LC0, %esi
       movl    $1, %edi
       xorl    %eax, %eax
       leal    1(%rbx), %edx//将rbx寄存器加1赋值给edx寄存器也是ss变量.                                                                      //rbx是64位寄存器,它的低32位是ebx寄存器.
       call    __printf_chk
       leal    2(%rbx), %edx//直接取rbx寄存器,加2赋值给foo1变量
       movl    $.LC1, %esi
       movl    $1, %edi
       xorl    %eax, %eax
        call   __printf_chk
       xorl    %eax, %eax
       popq    %rbx
       .cfi_def_cfa_offset 8
       ret
       .cfi_endproc

 

结论:第一实验加入了内存屏障中,两次分别从内存中加载foo变量到寄存器中,然后进行操作,第二个实验中取出了内存屏障,第一次将foo变量从内存中加载到了寄存器中,进行第一次操作,然后第二次并没有从内存中加载,而是直接去寄存器中的值去操作,由此可以看出屏障的后面是不可以缓存变量在寄存器中的,而是屏障后面的变量是需要重新从主存中加载的.

3.2 Java的内存屏障

从上文可知:一个load操作需要进入loadbuffer中的,然后在从内存中去加载,一个store操作需要进入storebuffer然后在写入内存.而两个buffer之间是异步,导致出现了不同的乱序(重排),java定义了一系列的内存屏障来指定指令的执行顺序.

Java中的内存中的内存屏障:

 

LoadLoad 屏障

StoreStore屏障

LoadStore 屏障

 

StoreLoad屏障

序列

Load1,Loadload,Load2

Store1,StoreStore,Store2

Load1,LoadStore,Store

Store1,StoreLoad,Load

作用

保证Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。

保证Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见

确保Load1的数据在Store2和后续Store指令被刷新之前读取。

确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。

对buffer的影响

在Load buffer插入屏障,清空屏障之前的Load操作,然后才能执行屏障之后的Load操作.

在Store buffer插入屏障,清空屏障之前的Store操作,然后才能执行屏障之后的store操作.

在Load buffer插入屏障,清空屏障之前的Load操作,然后才能执行屏障之后的Store操作.

在Load buffer, Store buffer中都插入屏障,必须清空屏障之前的Load操作并且清空屏障之前的store操作,然后才能执行屏障之后的Load操作,或store操作.

 

Store

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值