final 和 volatile的关系

之前的一片文章final 变量可变欠下了一个技术债,就是反射修改final变量值,到最后调用了Unsafe.putObjectVolatile方法,看方法名字中包含了"Volatile",那么推论Volatile和final有渊源,或者说在内存语义上有关联。那么这种关联是什么呢,下面来分析一下。先我们看一些概念。

volatile变量写的概念如下:在Java 并发编程这本书中,写到如果对一个volatile 写,在写的前后会插入内存屏障,如下示意图:

StoreStore就是说上面的写和volatile变量的写不能重排序,StoreLoad意思是votatile变量的写和下面的读不能重排序。 

final 变量写的内存语义,在Java 并发编程这本书中是这么定义的写final 域的重排序规则禁止把final域的写重排序到构造函数之外。就是说final变量在构造函数初始化之后,对其他线程可见。final 变量通过反射改变了值,要保证改变的值对其他线程可见,可以有两种做法,一种是锁,一种是利用内存屏障(内存屏障就是静止重排序的指令,下面一篇文章解释各个内存屏障的含义),根据性能看内存屏障性能更高,而正好有现成的机制实现这个线程的安全,就是volatile的写操作的内存语义实现的机制。putObjectVolatile的源码其实在unsafe.cpp的实现是Unsafe_SetObjectVolatile:

UNSAFE_ENTRY(void, Unsafe_SetObjectVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject x_h))
  UnsafeWrapper("Unsafe_SetObjectVolatile");
  oop x = JNIHandles::resolve(x_h);
  oop p = JNIHandles::resolve(obj);
  void* addr = index_oop_from_field_offset_long(p, offset);
  OrderAccess::release();
  if (UseCompressedOops) {
    oop_store((narrowOop*)addr, x);
  } else {
    oop_store((oop*)addr, x);
  }
  OrderAccess::fence();
UNSAFE_END

为了查看区别,贴上普通修改的代码

UNSAFE_ENTRY(void, Unsafe_SetObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject x_h))
  UnsafeWrapper("Unsafe_SetObject");
  oop x = JNIHandles::resolve(x_h);
  oop p = JNIHandles::resolve(obj);
  if (UseCompressedOops) {
    oop_store((narrowOop*)index_oop_from_field_offset_long(p, offset), x);
  } else {
    oop_store((oop*)index_oop_from_field_offset_long(p, offset), x);
  }
UNSAFE_END

可以看到区别就是oop_store操作被OrderAccess::releases()和OrderAccess:fence()包围了,那么这对方法是干嘛的呢,我们继续查找。在orderAccess.hpp中我们查找了方法定义,我们先看一段类注释

// This interface is based on the JSR-133 Cookbook for Compiler Writers
// and on the IA64 memory model.  It is the dynamic equivalent of the
// C/C++ volatile specifier.  I.e., volatility restricts compile-time
// memory access reordering in a way similar to what we want to occur
// at runtime.

大概意思是说接口建立在JSR-133的规范和IA64的内存模型,IA64是Intel 安藤系列的处理器。不管这个,但JSR-133得关注下,有时间研究下这个规范,主要是针对并发的一个规范,指令重排,内存屏障的概念都有讲到,内存屏障。再贴出如下注释:

//               sparc RMO             ia64             x86
// ---------------------------------------------------------------------
// fence         membar #LoadStore |   mf               lock addl 0,(sp)
//                      #StoreStore |
//                      #LoadLoad |
//                      #StoreLoad
//
// release       membar #LoadStore |   st.rel [sp]=r0   movl $0,<dummy>
//                      #StoreStore
//               st %g0,[]
//
// acquire       ld [%sp],%g0          ld.acq <r>=[sp]  movl (sp),<r>
//               membar #LoadLoad |
//                      #LoadStore
//
// release_store membar #LoadStore |   st.rel           <store>
//                      #StoreStore
//               st
//
// store_fence   st                    st               lock xchg
//               fence                 mf
//
// load_acquire  ld                    ld.acq           <load>
//               membar #LoadLoad |
//                      #LoadStore

定义方法所对应的内存屏障。然后再贴出方法定义:

class OrderAccess : AllStatic {
 public:
  static void     loadload();
  static void     storestore();
  static void     loadstore();
  static void     storeload();

  static void     acquire();
  static void     release();
  static void     fence();    ...

跟踪到这里,基本了解了其思想,就是插入一堆内存屏障保证内存可见性。

总结:虽然反射能修改final变量,但为了保证内存语义,对线程的可见性,利用了volatile插入内存屏障的机制。这样还了上篇的技术债。同时这里推荐《Java 并发编程的艺术》这本书,虽然不厚,但讲的都是精要,通过这本书,等于是学习了java 的并发包。

猜你喜欢

转载自blog.csdn.net/hanshengjian/article/details/86612767