// Single threaded versionclass Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { helper = new Helper(); } return helper; } // other functions and members...}
这段在使用多线程的情况下无法正常工作。在多个线程同时调用getHelper()时,必须要获取,否则,这些线程可能同时去创建对象,或者某个线程会得到一个未完全初始化的对象。
锁可以通过代价很高的同步来获得,就像下面的例子一样。
// Correct but possibly expensive multithreaded versionclass Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) { helper = new Helper(); } return helper; } // other functions and members...}
只有getHelper()的第一次调用需要同步创建对象,创建之后getHelper()只是简单的返回成员变量,而这里是无需同步的。 由于同步一个方法会降低100倍或更高的性能, 每次调用获取和释放锁的开销似乎是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。许多程序员一下面这种方式进行优化:
- 检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。
- 获取锁
- 第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。
- 否则,初始化并返回变量。
// Broken multithreaded version// "Double-Checked Locking" idiomclass 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...}
直觉上,这个算法看起来像是该问题的有效解决方案。然而,这一技术还有许多需要避免的细微问题。例如,考虑下面的事件序列:
- 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
- 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。
- 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存()),程序很可能会崩溃。
在或更早的版本中使用双重检查锁有潜在的危险,有时会正常工作:区分正确和有小问题的实现是很困难的。取决于,线程的和其他,不正确的实现双重检查锁导致的异常结果可能会间歇性出现。重现异常是十分困难的。
在中,这一问题被修正了。关键字保证多个线程可以正确处理单件实例。描述了这一新的语言特性:
// Works with acquire/release semantics for volatile// Broken under Java 1.4 and earlier semantics for volatileclass Foo { private volatile Helper helper = null; public Helper getHelper() { Helper result = helper; if (result == null) { synchronized(this) { result = helper; if (result == null) { helper = result = new Helper(); } } } return result; } // other functions and members...}
注意局部变量result的使用看起来是不必要的。对于某些版本的,这会使代码提速25%,而对其他的版本则无关痛痒。
如果helper对象是静态的(每个类只有一个), 可以使用双重检查锁的替代模式模式。查看 上的列表16.6。
// Correct lazy initialization in Java@ThreadSafeclass Foo { private static class HelperHolder { public static Helper helper = new Helper(); } public static Helper getHelper() { return HelperHolder.helper; }}
这是因为内部类直到他们被引用时才会加载。
Java 5中的final语义可以不使用volatile关键字实现安全的创建对象:
public class FinalWrapper{ public final T value; public FinalWrapper(T value) { this.value = value; }} public class Foo { private FinalWrapper helperWrapper = null; public Helper getHelper() { FinalWrapper wrapper = helperWrapper; if (wrapper == null) { synchronized(this) { if (helperWrapper == null) { helperWrapper = new FinalWrapper (new Helper()); } wrapper = helperWrapper; } } return wrapper.value; }}
为了正确性,局部变量wrapper 是必须的。这一实现的性能不一定比使用volatile 的性能更高。