之前的一片文章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 的并发包。