在并发问题中, 还有一个比较关键的问题就是指令重排序 (在不改变程序语义的前提下,CPU为了更高效地利用缓存或寄存器), 但这同时会带来一些并发问题,造成一些数据不完整,不一致等问题, 因此JMM(Java Memory Model)就规范了一些Happen-Before的偏序关系, 以避免这些并发问题。
重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。
编译源代码时,编译器依据对上下文的分析,对指令进行重排序。
CPU在执行命令过程中,对指令进行重排序。
Happen-Before指的是两个操作之前的一种偏序关系,比如,操作A happen-before 操作B, 那么操作A对于操作B是可见的(即操作A产生的影响会作用于操作B,如共享变量更新,消息发送,方法调用等),记作hb(A, B)。
1. i = 1; // T1
2. j = i; // T2
3. i = 2; // T3
操作1, 2, 3分别在线程T1, T2, T3中执行,若有偏序关系hb(1, 2),即T1的操作1对T2的操作2是可见的, 那么,T2的操作2执行后,变量j的值一定为1(此时T3的操作3还未执行),但若T3的操作3发生在操作1,2之间, 就不能确定最终i的值是多少,因为操作3,2并没有偏序关系,因此操作3的结果有可能影响操作2,也有可能不会。
1. 程序次序规则: 同一线程内,前面的动作hb后面的动作。
2. 管程锁定规则: 对于同一个锁,unlock()操作hb下一次lock()操作。
3. volatile变量规则: volatile变量的写操作hb之后的读操作。
4. 线程启动规则: 同一线程的start()方法hb其他方法。
5. 线程终止规则: 同一线程的任何方法都hb对此线程的终止检测。如Thread.join(),Thread.isAlive()等。
6. 线程中断规则: 对线程interrupt()方法的调用hb
发生于被中断线程的代码检测到中断事件发生。
7. 对象终结规则: 一个对象的初始化完成hb该对象的finalize()方法。
8. 传递性: A操作hb操作B,操作Bhb操作C,那么A操作hb操作C。
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
}
class Foo {
private Helper helper = null;
public synchronized Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
}
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
}
最明显的原因是Helper对象的构造函数执行操作 和helper字段的赋值操作有可能被重排序。 因此,调用getHelper()方法的线程虽然看到helper对象为非null, 但仅看到了helper对象各字段的默认值,而没有看到构造函数执行后的helper对象。
如果编译器内联地调用构造函数,只要构造函数不抛出异常或者进行同步操作, Helper对象的构造函数执行操作 和helper字段的赋值操作就可以自由排序, 即使编译器不排序这些写操作,在多处理器系统中也有可能进行排序。
singletons[i].reference = new Singleton();
0206106A mov eax,0F97E78h
0206106F call 01F6B210 ; allocate space for
; Singleton, return result in eax
02061074 mov dword ptr [ebp],eax ; EBP is &singletons[i].reference
; store the unconstructed object here.
02061077 mov ecx,dword ptr [eax] ; dereference the handle to
; get the raw pointer
02061079 mov dword ptr [ecx],100h ; Next 4 lines are
0206107F mov dword ptr [ecx+4],200h ; Singleton's inlined constructor
02061086 mov dword ptr [ecx+8],400h
0206108D mov dword ptr [ecx+0Ch],0F84030h
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
Helper h;
synchronized(this) {
h = helper;
if (h == null) {
synchronized (this) {
h = new Helper();
} // monitor被释放
helper = h;
}
}
}
return helper;
}
}
这里将Helper对象的构造过程放在内部锁中, 目的就是阻止 Helper对象初始化操作和 helper字段被赋值操作被重排序。 然而,上面的代码仍然不能正常工作,同步规则并不会以那种方式工作, monitorexit字节码指令的规则(如,释放锁)是, 在monitor被释放前,monitorexit之前的动作必须被执行,但却没有规则说, monitorexit之后的动作不可能在monitor被释放前被执行, 也就是说,对于编译器而言,移动helper = h;声明完全是合理的。 很多处理器都提供这种单向内存屏障,若将请求锁的语义改变为完全内存屏障将带来性能损失。
虽然可以强制使用完全内存屏障,但这效率性能极低,并且一旦JMM被修改,很有可能不能正常工作, 所以不要这么做。然而即便在初始化helper字段时,使用了完全内存屏障,仍然不能正常工作。 因为,CPU都有自己本地的内存缓存副本,在某些处理器上,即便写操作通过内存屏障直接写到主内存中, 读操作也必须需要执行缓存一致性指令,否则读操作将从CPU缓存中读取, 比如Alpha处理器。
class HelperSingleton {
static Helper singleton = new Helper();
}
class Foo {
private final ThreadLocal perThreadInstance = new ThreadLocal();
private Helper helper = null;
public Helper getHelper() {
if (perThreadInstance.get() == null) createHelper();
return helper;
}
private final void createHelper() {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
perThreadInstance.set(perThreadInstance);
}
}
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
http://www.cs.umd.edu/~pugh/java/memoryModel/BidirectionalMemoryBarrier.html
http://www.cs.umd.edu/~pugh/java/memoryModel/AlphaReordering.html
http://blog.csdn.net/ns_code/article/details/17348313
<<深入理解Java虚拟机>>