您可以通过消除延迟初始化风险(您目前在例外情况下为此付出代价)来避免一些混淆。既然你真的只是返回你的静态实例,为什么不在运行时创建那个静态实例(即,不要等待 null):
class MySingleton {
private static MySingleton instance = new MySingleton();
// You could do this here or in the constructor
// private OtherObject obj = new OtherObject();
/** Use this if you want to do it in the constructor instead. */
private OtherObject obj;
private MySingleton() {
obj = new OtherObject();
}
/** Now you can just return your static reference */
public static MySingleton getInstance() {
return instance;
}
}
如果您注意到,现在一切都是确定性和直接的。您的 MySingleton.instance 在运行时填充并通过静态方法访问MySingleton.getInstance()
。
我意识到这与原始 GOF 设计模式书的确切模式不匹配,但您会注意到该类的用法实际上是相同的。
编辑:跟进其他答案和评论中提出的一些线程安全点,我将尝试说明问题和 GOF 书中最初提出的解决方案如何是非线程安全的。如需更好的参考,请参阅我在 ACM / Safari 在线书架上拥有并拥有的Java 并发实践。坦率地说,它比我草拟的例子更好,但是,嘿,我们只能努力....
因此,假设我们有两个线程,分别命名为 Thread1 和 Thread2,巧合的是,每个线程同时都调用了 MySingleton.getInstance() 方法。这在现代多核超线程系统中是完全可能的。现在,即使这两个线程碰巧同时到达了这个方法的入口点,也不能保证哪个线程会到达任何特定的语句。所以,你可以看到这样的东西:
// Note that instance == null as we've never called the lazy getInstance() before.
Thread1: if (instance == null)
Thread2: if (instance == null) // both tests pass - DANGER
Thread1: instance = new MySingleton();
Thread2: instance = new MySingleton(); // Note - you just called the constructor twice
Thread1: return instance; // Two singletons were actually created
Thread2: return instance; // Any side-effects in the constructor were called twice
if-null 测试中说明的问题称为竞争条件。你无法知道谁会在什么时候发表什么声明。结果,就像两条线都在相互竞争到悬崖上。
如果您对我的示例进行相同类型的检查,两个线程同时仍在访问 getInstance(),您会看到如下内容:
Thread1: return instance;
Thread2: return instance;
简单化,是的,但这就是我的观点。该对象是很久以前构造的,线程可以指望它的值是一致的。不存在种族。
注意:您仍然需要担心 OtherObject 的构造函数的内容。那里的教训是:并发很难。如果您使构造函数线程安全(您应该这样做),请确保 OtherObject 的作者对您也有同样的礼貌。