深入volatile到hotspot源码&屏障

275 阅读5分钟
  • 从各种资料中经常能看到volatile是通过lock:前缀或是加内存屏障保证的可见性有序性,甚至还有说是加了volatile触发MESI协议的。

那么到底是什么样的,我们通过阅读源码查看

image.png

  • 反编译看看

image.png ACC_VOLATILES 表示int abc字段被volatile修饰 在main方法中修改abc字段使用了putstatic,操作数据类型是I,那么我们进入cpp源码看看

putstatic()

CASE(_putstatic): //_putstatic
        {
        //这里删除了前面一些源码
          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {  //判断字段是否是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) {
              obj->release_byte_field_put(field_offset, STACK_INT(-1)); //压栈
            } else if (tos_type == ltos) {
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            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) {
              obj->byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {
              obj->long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {
              obj->char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {
              obj->short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {
              obj->float_field_put(field_offset, STACK_FLOAT(-1));
            } else {
              obj->double_field_put(field_offset, STACK_DOUBLE(-1));
            }
          }

          UPDATE_PC_AND_TOS_AND_CONTINUE(3, count); //更新程序计数器
        }
  • 可以看到被volatile修饰的字段在执行putstatic操作时最终执行了storeload(),这是一个万能屏障,开销最大

(linuxX86)

inline void OrderAccess::storeload()  { fence(); } 
  • 内部又执行了fence(),接着往下跟,这里出现了我们在各种资料中看到的lock前缀
inline void OrderAccess::fence() {
  if (os::is_MP()) { //检查是否是多处理器操作系统,如果是单处理器没有必要加屏障
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}
  • __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");:这是内联汇编语句,实现了一个内存屏障。在 x86_64 架构下,它使用了 lock 前缀和 addl 指令来执行一个原子加法操作,但实际上加的是 0,这样做的目的是为了在执行过程中触发内存屏障效果。

    • lock 前缀给缓存行加锁,如果操作数据不在缓存或涉及多个缓存行发送LOCK#信号加总线锁
    • lock 前缀可以保证对指令的读写是原子性的,保证了指令的原子性。
    • lock 前缀不直接导致缓存行失效
    • addl $0,0(%%rsp) 指令实际上不会做任何数学上的加法,它只是将栈顶指针所指向的内存地址处的值加上 0,但由于使用了 lock 前缀,这个操作会导致所有处理器暂停其它对共享内存的访问,从而实现了内存屏障的效果。
  • : : : "cc", "memory":这是内联汇编语句的输出、输入和破坏列表。"cc" 表示指令可能会修改标志寄存器(condition codes),因此需要告知编译器。"memory" 表示这个汇编语句可能会影响内存中的数据,因此编译器需要刷新所有内存操作的缓存

  • 如果在内联汇编中使用了 memory 修饰符,编译器会在生成的机器代码中插入相应的缓存一致性操作,以确保内存操作的一致性和可见性。这些缓存一致性操作可以包括使得处理器缓存中的数据失效,以便下次访问时重新从主内存中读取最新的数据。

getstatic

  • 那读呢,volatile是怎么保证读的可见性的
CASE(_getstatic):
        { //省略部分源码
          if (cache->is_volatile()) { //判断是否是volatile修饰 
            if (tos_type == atos) {
              VERIFY_OOP(obj->obj_field_acquire(field_offset));
              SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
            } else if (tos_type == itos) {
              SET_STACK_INT(obj->int_field_acquire(field_offset), -1); //我们是int类型,进这里看看
            } else if (tos_type == ltos) {
              SET_STACK_LONG(obj->long_field_acquire(field_offset), 0);
              MORE_STACK(1);  //细节long类型
            } else if (tos_type == btos) {
              SET_STACK_INT(obj->byte_field_acquire(field_offset), -1);
            } else if (tos_type == ctos) {
              SET_STACK_INT(obj->char_field_acquire(field_offset), -1);
            } else if (tos_type == stos) {
              SET_STACK_INT(obj->short_field_acquire(field_offset), -1);
            } else if (tos_type == ftos) {
              SET_STACK_FLOAT(obj->float_field_acquire(field_offset), -1);
            } else {
              SET_STACK_DOUBLE(obj->double_field_acquire(field_offset), 0);
              MORE_STACK(1);
            }
          } else {
            if (tos_type == atos) {
              VERIFY_OOP(obj->obj_field(field_offset));
              SET_STACK_OBJECT(obj->obj_field(field_offset), -1);
            } else if (tos_type == itos) {
              SET_STACK_INT(obj->int_field(field_offset), -1);
            } else if (tos_type == ltos) {
              SET_STACK_LONG(obj->long_field(field_offset), 0);
              MORE_STACK(1);
            } else if (tos_type == btos) {
              SET_STACK_INT(obj->byte_field(field_offset), -1);
            } else if (tos_type == ctos) {
              SET_STACK_INT(obj->char_field(field_offset), -1);
            } else if (tos_type == stos) {
              SET_STACK_INT(obj->short_field(field_offset), -1);
            } else if (tos_type == ftos) {
              SET_STACK_FLOAT(obj->float_field(field_offset), -1);
            } else {
              SET_STACK_DOUBLE(obj->double_field(field_offset), 0);
              MORE_STACK(1);
            }
          }

          UPDATE_PC_AND_CONTINUE(3);
         }
inline jint oopDesc::int_field_acquire(int offset) const{
        return OrderAccess::load_acquire(int_field_addr(offset));
}

继续往下追

inline jint     OrderAccess::load_acquire(volatile jint*    p) { return *p; }

原来是使用了C++的volatile,在 C++ 中,volatile 关键字的实现通常涉及到以下几个方面:

  1. 编译器优化:当一个变量被声明为 volatile 时,编译器会生成对该变量的读写操作的代码,而不会对这些操作进行优化。这意味着编译器不会将对 volatile 变量的读写操作缓存到寄存器中,也不会对读取操作进行优化,以确保变量的读写操作的顺序性和可见性。
  2. 内存屏障:在一些平台上,编译器会在对 volatile 变量进行读写操作时插入适当的内存屏障(memory barrier),以确保变量的读写操作不会被重排序,也保证了对变量的读写操作在指定位置之前完成。内存屏障的具体实现依赖于编译器和目标平台。
  3. 强制读取:在某些情况下,编译器可能会生成强制读取(force read)的代码,以确保每次访问 volatile 变量时都从内存中读取最新的值,而不是使用缓存中的旧值。

其他几种屏障

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

inline void OrderAccess::acquire() {
  volatile intptr_t local_dummy;
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}

inline void OrderAccess::release() {
  // Avoid hitting the same cache-line from
  // different threads.
  volatile jint local_dummy = 0;
}

inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); //memory字段,告诉编译器这段汇编代码可能会修改内存中的数据,会使缓存行失效,保证共享数据可见性
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}