引述[0]
双检测机制主要用于多线程环境下的延迟初始化,也经常和单件(Singleton)模式在一起使用。如果只讨论Singleton模式,不必这么麻烦。
下面是一段双检测锁代码:
// 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...
}
双检测机制不起作用的原因是对象(Helper)的创建和成员(helper)的初始化顺序是不确定的。比如,线程调用方法getHelper(),可以获取一个非空的helper的引用,但是可能发现helper对象成员的值不是正确的应该在构造函数执行之后的值。如果创建对象和初始化对象是一步执行(inline)的,那两者的顺序就无所谓。
在Symantec JIT的虚拟机上的测试验证了双检测的问题。比如下面的语句:
singletons[i].reference = new Singleton();
赋值操作是在构造函数之前执行的。
在Singleton模式下,如果可以只有一个对象,可以定义静态成员。文中给的例子是在一个单独的类中定义:
class HelperSingleton {
static Helper singleton = new Helper();
}
还可以使用线程本地存储(Thread Local Storage)解决双检测机制
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);
}
}
上面的代码显示可以保证每个线程的getHelper()是安全的和正确的。在Java5及以后的版本里,采用了新的内存模型,可
以使用volatile关键字修饰变量如下:
// 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;
}
}
给变量添加volatile关键字,指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。而不是本地内存(比如机器的寄存器)。双检测机制联合使用volatile后,系统不允许对变量的写操作和之前的读写操作进行优化重排执行(reorder),同时,不会对读操作和之前的读写操作进行优化重排执行。
( the system will not allow a write of a volatile to be reordered with respect to any previous read or write, and a read of a volatile cannot be reordered with respect to any following read or write.)
结合代码,如果helper已经new出来,但是还没有运行构造函数,此时另外一个线程的访问会延后到构造函数执行完毕才成功。就是说如果已经开始对helper进行写操作(new及构造初始化),则读操作(获取引用)不会成功。也就是说,volatile 变量可以被看作是一种 “程度较轻的 synchronized。
一个极端的例子,如果对象是一个不可变对象(immutable object),所有的成员都是final的,双检测机制可以工作的很好,因为访问一个不可变对象和访问原生类型,比如int,float等32位对象是没有什么区别的,都是原子操作。
总结实现考虑
1. 如果只是在Singleton模式下,可以简单使用静态变量,不适用延迟加载,不会有任何问题
2. 如果一定想延迟加载,加上volalite关键字
3. 如果不记得volalite关键字,使用同步方式,不使用双检测。
别的语言
1. C++: 可以结合内存栅栏(memory barriers),思路类似Java中volalite的效果。代码好似从ACE中摘的,但是我看最新的ACE代码,Singleton实现没有发现这样使用,这个问题暂且存疑。
2. Erlang:上面说了这么多,这个问题在Erlang中完全不存在。Erlang的变量不会改变,程序逻辑的流转依靠消息。这也算是Erlang的一个优势吧。
参考:
0. The "Double-Checked Locking is Broken" Declaration http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
1. 正确使用volatile: http://www.ibm.com/developerworks/cn/java/j-jtp06197.html