Double-Checked Locking方法被普遍的使用于实现多线程环境下单例模式的懒加载方式实现,不幸的是,在JAVA中,这种方式有可能不可以正常工做。在其余语言环境中,如C++,依赖于处理器的内存模型、编译器的重排序以及编译器和同步库之间的工做方式。因为这些问题在C++中并不肯定,所以咱们不可以肯定具体的行为。可是在C++中显示的内存屏障是能够被用来让其正常工做的,而这些屏障在JAVA中又很差用。
html
首先来看看下面这段代码咱们指望获得的行为:
java
// Single threaded version class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... }
这段代码若是运行在多线程环境下,将会出现问题。很显然的一个问题,两个或者多个Helper对象将会被分配内存,其余问题咱们会在后面提到,咱们先简单的给方法加一个synchronized关键字。
缓存
// Correct multithreaded version class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... }
上面的代码在每次调用getHelper方法的时候都要进行同步,下面的Double-Checked Locking方式避免了当Helper对象被实例化以后再次进行同步:
多线程
// Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) helper = new Helper(); } return helper; } // other functions and members... }
不幸的是,这段代码在存在编译优化或多处理器共享内存的状况下不可以正常工做。ide
为何上文说Double-Checked Locking不可以正常工做有不少的缘由,咱们将会描述一对很显而易见的缘由。经过理解存在的问题,咱们尝试着去修复Double-Checked Locking存在的问题,然而咱们的修复可能并无用,咱们能够一块儿看看为何没有用,理解这些缘由,咱们去尝试着寻找更好的方法,可能仍是没有用,由于仍是存在一些微妙的缘由。函数
Double-Checked Locking不可以正常工做的一个很显然的缘由是对helper属性的写指令和初始化Helper对象的指令可能被冲排序,所以当其余线程再次调用getHelper方法的时候,将会获得一个没有被初始化完成的Helper对象,若是这个线程访问了这个对象没有被初始化的属性,那么就会出现位置错误。性能
咱们来看看对于下面这行代码,在Symantec JIT编译器环境下的指令重排序的例子:优化
singletons[i].reference = new Singleton();
下面是实际执行的代码:ui
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
咱们能够看到对于singletons[i].reference的赋值操做是在构造Singleton对象以前,这在当前的JAVA内存模型中是彻底合法的,在C和C++中也是合法的。
this
理解了上面的问题,有些同窗给出了下面的这段代码,试图避免问题:
// (Still) Broken multithreaded version // "Double-Checked Locking" idiom 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(); } // release inner synchronization lock helper = h; } } return helper; } // other functions and members... }
上面的代码将对象构造放在一个内部的synchronized块里面,直觉的想法是想经过synchronized释放以后的屏障来避免问题,从而阻止对helper属性的赋值和对Helper对象的构造的指令重排序。不幸的是,直觉是错误的。由于synchronization的规则能保证全部在monitorexit以前的动做都可以生效而并不包含在monitorexit以后的动做在monitorexit以前不生效。也就是咱们可以保证在退出内部同步块以前Helper可以被实例化,h可以被复制,可是不能保证helper被赋值必定发生在退出同步块以后,所以一样会出现没有被构造完的Helper实例被其余线程引用并访问。
咱们能够经过彻底双向的内存屏障来强制行为生效,这么作是粗鲁的,非高效的,而且几乎能够保证一旦JAVA内存模型被修订,原有方式将不可以正常工做。因此,请不要这么作。然而,即便经过彻底内存屏障,仍是不可以正常工做。问题是在一些系统上,线程对非空的helper属性字段一样须要内存屏障。为何呢?由于处理器拥有本身的缓存,在一些处理器中,除非处理器执行缓存一致性指令,不然将有可能从缓存读取错误内容,尽管其余处理器将内容从缓存刷新到了主存。
在不少应用中,简单的将getHelper方法同步开销其实并不大,除非可以证实其余优化方案确实可以为应用带来很多的性能提高。
若是咱们正要建立的实例是static的,咱们有一种很简单的方法,仅仅将单例静态属性字段在一个单独的类中定义:
class HelperSingleton { static Helper singleton = new Helper(); }
这么作既保证的懒加载,又保证单例被引用的时候已经被构造完成。
尽管Double-Checked Locking对对象引用类型无效,对于32位原始类型倒是有效的,值得注意的是对64位的long和double类型并非有效的,由于64为的long和double不可以保证被原子地读写。
// Correct Double-Checked Locking for 32-bit primitives class Foo { private int cachedHashCode = 0; public int hashCode() { int h = cachedHashCode; if (h == 0) synchronized(this) { if (cachedHashCode != 0) return cachedHashCode; h = computeHashCode(); cachedHashCode = h; } return h; } // other functions and members... }
实际上,假设computeHashCode函数老是有固定的返回值,咱们能够不使用同步块:
// Lazy initialization 32-bit primitives // Thread-safe if computeHashCode is idempotent class Foo { private int cachedHashCode = 0; public int hashCode() { int h = cachedHashCode; if (h == 0) { h = computeHashCode(); cachedHashCode = h; } return h; } // other functions and members... }
Alexander Terekhov提出了一个聪明的方法,经过ThreadLocal来实现Double-Checked Locking,每一个Thread保持一个local flag来标识当前线程是否已经进入过同步块:
class Foo { /** If perThreadInstance.get() returns a non-null value, this thread has done synchronization needed to see initialization of helper */ 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(); } // Any non-null value would do as the argument here perThreadInstance.set(perThreadInstance); } }
这种方式的性能取决于JDK版本,在Sun公司的JDK1.2版本中,ThreadLocal是很慢的,在1.3版本以后变得很是快了。
在JDK1.5或者更晚的版本中,扩展了volatile的语义,使得咱们能够经过将helper属性字段设置为volatile来修复Double-Checked的问题:
// Works with acquire/release semantics for volatile // Broken under current semantics for volatile class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) helper = new Helper(); } } return helper; } }
还有一种方法是讲单例对象变为不可变对象,如全部字段都声明为final或者相似String类或Integer类这种。
本文由博主翻译改编自:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 欢迎转载,若有内容错误,还请多多包涵。