volatile关键字详解

这个关键字我得看了十几篇博客,才终于有点底气来写这篇博客了。一些人写得的博客真的是呵呵了,说的很多都是错的。看了十几篇博客还问了大牛才懂得具体细节。


其实volatile关键字很简单,就是在所修饰变量出现写操作后加入一个内存屏障

看到一篇文章写得不错,把volatile已经解释得很清楚了。如下
http://www.cnblogs.com/dolphin0520/p/3920373.html

读者先看完上面那篇文章,然后我在这里再详细讲讲内存屏障

内存屏障,也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

总结起来就两个作用:

  • 内存屏障前的程序一定会执行完毕再执行内存屏障后的程序(抑制重排序)
  • 内存屏障前的写操作一定会被写入内存再执行屏障后的程序(内存可见性)

C++里面有一个mb()函数,其在 linux/include/asm-i386/system.h定义:

#define mb()__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")  

这行代码就是内存屏障

  1. __asm__用于指示编译器在此插入汇编语句
  2. __volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
  3. “memory” 强制编译器假设RAM所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。
  4. lock前缀表示将后面这句汇编语句:” addl $0,0(%%esp)”作为cpu的一个内存屏障。
  5. addl $0,0(%%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp寄存器的数值依然不变。

那么在java中呢?:
例如你让一个volatile的integer自增(i++),其实要分成3步:

  1. 读取volatile变量值到local;
  2. 增加变量的值;
  3. 把local的值写回,让其它的线程可见。

在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。这3步的指令为:

mov    0xc(%r10),%r8d ;     Load
inc    %r8d           ;     Increment
mov    %r8d,0xc(%r10) ;     Store
lock addl $0x0,(%rsp) ;     StoreLoad Barrier

有volatile变量修饰的共享变量进行写操作的时候会多第四行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

这个作用和前面的mb()函数非常相似

接下来引用自下面这篇文章:深入分析Volatile的实现原理

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
这两件事情在IA-32软件开发者架构手册的第三册的多处理器管理章节(第八章)中有详细阐述。

volatile的使用非常敏感,只能在特定的情况下使用,具体可看下面这篇文章
https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

为什么说有限制呢?例如上面的用volatile修饰的i++在多线程情况下可能会的不到我们想要的效果:

    private static int i;
    public static void main(String[] args) throws Exception {
       Runnable runanle = new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 10000; j++) {
                    i++;
                }
            }
        };
        new Thread(runanle).start();
        new Thread(runanle).start();
        Thread.sleep(3000); //防止线程还未执行结束
        System.out.println(i);
    }

输出结果很大可能小于20000,这时因为

volatile没有原子性,不要将volatile用在getAndOperate场合,仅仅set或者get的场景是适合volatile的

从Load到store到内存屏障,一共4步,其中最后一步jvm强制执行完上面的代码,并让缓存的内容写到内存,并让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

例如某个时候线程执行到如下阶段,假如这个时候i = 10;i的值被放到%r8d寄存器中,并自增到了11。可是现在还没将数据写会缓存和内存,就被其他线程抢占了

mov    0xc(%r10),%r8d ;     Load
inc    %r8d           ;     Increment

另外一个线程就开始执行,这个时候它并不知道上个线程已经将i加到了11, 它在内存或者自己的缓存获取到i = 10;开始自己的自增操作并将i=11写入到内存.
接着上面的线程继续执行又将i=11写入到内存。于是这样就出现了我们不期望的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值