volatile关键字详解

volatile

volatile作用简述

volatile是java自带的关键字,其作用是通过防止指令重排和缓存一致性协议,保证多线程并发下的可见性问题。指令重排是指,在不影响代码执行的最终结果前提下,为了最大化cpu利用率以及性能,将代码乱序执行。

volatile实现的原理

解决缓存一致性

确切来讲,volatile并不能保证缓存一致性,缓存一致性是通过硬件层面的缓存一致性协议保证的,例如MESI协议。但是有些硬件是具有缓存强一致性的,也就是说在这些平台上,即使是没有被volatile修饰的变量也不会存在缓存一致性问题,在这些平台上面,volatile只是一条空指令。

保证缓存一致性主要有两种方法,一种是cpu的缓存锁(总线锁),一种是缓存一致性协议。缓存锁的作用原理是,当发起读写操作时,处理器会发出一个lock指令锁住总线,使当前处理器独享内存,其他处理器在lock期间的请求将会被阻塞。所以,这种方法会导致性能问题。缓存一致性协议是指通过一定的协议,解决缓存一致性问题,至于如何具体解决,这就要去了解各种缓存一致性协议的具体实现。

缓存一致性协议最出名的是Intel的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

防止指令重排

指令重排分有两种,一种是编译器的优化乱序,一种是cpu的执行乱序。对于这两种种情况,可以通过优化屏障和内存屏障。

内存屏障,也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。内存屏障只解决顺序一致性问题,不解决缓存一致性问题,缓存一致性问题是有cpu的缓存锁或者缓存一致性协议保证的。

CPU层的内存屏障

CPU级别的内存屏障有三种,分别为写屏障(store barrier),读屏障(load barrier)和全屏障(full barrier)。

  • 写屏障:强制所有在写屏障之前的指令,都要在写屏障之前执行,并发送缓存失效信号。所有在写屏障之后的store指令,都要在写屏障之后执行。也就是禁止屏障前的指令重排和屏障后的store指令重排。
  • 读屏障:强制所有在读屏障之后的load指令,都在读屏之后执行,也就是禁止读屏障之后的load操作的重排序。
  • 全屏障:全能型屏障。在全屏障前后的所有store/load操作,都要在对应的前后执行。也就是完全防止屏障前后的指令重排。
编译器层的内存屏障

和cpu内存屏障不同的是,编译器级别内存屏障分为四种,如图。
volatile关键字详解_第1张图片

在jvm源码里面,不同的内核对内存屏障的实现不同,例如在orderAccess_linux_x86.inline.hpp里面,定义了这四种内存屏障对应的调用方法:

inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }

从上面的代码可以看到,loadload barrier和loadstore barrier调用的方法都是一样的,也就是说,loadload barrier和storeload barrier的作用是一样的。

不带volatile和带volatile的变量生成的字节码比较

带volatile的变量字节码

private static volatile int num;
  descriptor: I
  flags: ACC_PRIVATE, ACC_STATIC, ACC_VOLATILE

不带volatile的变量字节码

private static int num;
  descriptor: I
  flags: ACC_PRIVATE, ACC_STATIC

从上面二者的字节码可以看出来,volatile修饰的变量多了个ACC_VOLATILE标志,而在accessFlags.hpp里面,定义了诸多判断java各种标识,其中就有bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; },这个方法就是判断变量是否是volatile变量。这个方法会在操作volatile变量的时候调用,例如在bytecodeInterpreter.hpp里面,当对一个变量执行写操作时,代码如下:

          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
            ...
            }
            OrderAccess::storeload();
          } else {
            if (tos_type == itos) {
              obj->int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {
              ...
            }
          }

可以看到,如果是volatile,会调用对应的带release开头的方法,并且在执行最后,会执行OrderAccess::storeload();这一行代码,也就是在每一次的volatile写之后插入一个storeload屏障。

扩展阅读

你可能感兴趣的:(多线程编程)