原因是线程安全。
你所说的那种形式可能会多次初始化单例,并且即使已多次初始化,不同线程对getInstance()
的后续调用也可能返回不同的实例!此外,一个线程可能会看到部分初始化的单例实例!(假设构造函数连接到数据库并进行身份验证;即使在构造函数中执行身份验证,一个线程也可能在其完成之前就能获得对单例的引用!)
处理线程时存在一些困难:
- 并发性:它们有可能同时执行;
- 可见性:由一个线程对内存所做的修改可能对其他线程不可见;
- 重排列:代码执行的顺序无法预测,这可能会导致非常奇怪的结果。
您应该学习这些困难,以确切地了解为什么这些奇怪的行为在JVM中是完全合法的,为什么它们实际上是好的,并且如何保护自己免受它们的影响。
静态块由JVM保证只会被执行一次(除非使用不同的ClassLoader
加载和初始化类,但细节超出了这个问题的范围),并且只由一个线程执行,并且它的结果保证对每个其他线程可见。
这就是为什么你应该在静态块中初始化单例。
我喜欢的模式:线程安全和延迟
上面的模式将在第一次执行看到对Map_en_US
类的引用时实例化单例(实际上,只有对类本身的引用将加载它,但可能尚未初始化它;有关详细信息,请检查引用)。也许你不想要那样。也许你只想在第一次调用Map_en_US.getInstance()
时初始化单例(就像你所说的那个模式所做的那样)。
如果您想要这样做,可以使用以下模式:
public class Singleton {
private Singleton() { ... }
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
上面的代码中,只有当类SingletonHolder
被初始化时,单例才会被实例化。这只会发生一次(除非,正如我之前所说,您使用多个类加载器),代码将由一个线程执行,结果不会有可见性问题,并且初始化只会在第一次引用SingletonHolder
时发生,它发生在getInstance()
方法内部。这是我需要单例时最常用的模式。
另外的模式...
1. synchronized getInstace()
如本答案评论中讨论的那样,有另一种以线程安全方式实现单例的方法,几乎与您熟悉的(有缺陷的)方法相同:
public class Singleton {
private static Singleton instance;
public static synchronized getInstance() {
if (instance == null)
instance = new Singleton();
}
}
以上代码是通过内存模型来保证线程安全的。JVM规范以更加晦涩的方式陈述了以下内容:假设L是任何对象的锁,T1和T2是两个线程。T1释放L的操作发生在T2获取L的操作之前。
这意味着,在释放锁之前,T1进行的所有操作都将对其他线程可见,这些线程在获取相同锁后都能看到。
因此,假设T1是第一个进入getInstance()
方法的线程。在它完成之前,没有其他线程能够进入相同的方法(因为它被同步了)。它将看到instance
为null,将创建一个Singleton
实例并存储在字段中。然后,它会释放锁并返回该实例。
接下来,等待锁的T2将能够获取并进入该方法。由于它获取了T1刚刚释放的相同锁,T2将看到字段instance
包含由T1创建的Singleton的完全相同的实例,并简单地返回它。更重要的是,由T1完成的单例初始化发生在T1释放锁之前,T1释放锁发生在T2获取锁之前,因此T2不会看到部分初始化的单例。
以上代码是完全正确的。唯一的问题是对单例的访问将被序列化。如果它经常发生,它将降低应用程序的可扩展性。这就是为什么我更喜欢我上面展示的SingletonHolder
模式:可以真正并发地访问单例,而无需同步!
2. 双重检查锁定(DCL)
通常,人们担心锁获取的成本。我读过现在它对大多数应用程序来说不那么重要。锁获取的真正问题是通过序列化对同步块的访问来损害可扩展性。
有人想出了一个聪明的方法来避免获取锁,并称之为双重检查锁定。问题是,大多数实现都是有缺陷的。也就是说,大多数实现不是线程安全的(即与原始问题中的getInstace()
方法一样不安全)。
实现DCL的正确方法如下:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
正确和不正确实现之间的区别在于
volatile
关键字。要理解为什么,让T1和T2成为两个线程。首先假设该字段不是
volatile
的。T1进入
getInstace()
方法,它是第一个进入该方法的线程,因此该字段为null。然后它进入同步块,然后进入第二个if。它也计算为true,因此T1创建单例的新实例并将其存储在字段中。锁定随后被释放,并返回单例。对于这个线程来说,保证单例完全初始化。现在,T2进入
getInstace()
方法。它可能(虽然不一定)看到
instance!= null
。然后它将跳过
if
块(因此它永远不会获得锁),并直接返回单例。由于重排序,T2可能无法看到单例构造函数中执行的所有初始化!重新访问db连接单例示例,T2可能会看到已连接但尚未进行身份验证的单例!建议阅读Java Concurrency in Practice和Java Language Specification获取更多信息。
getInstance
方法线程安全吗?! - GrubergetInstance()
声明为synchronized
,内存模型将保证单例的线程安全性:不会有并发初始化,并且如果 L 是任何对象的锁,T1 和 T2 是两个线程,则 T2 获取 L happens-before T1 释放 L,因此 T2 将看到持有单例的字段已填充,并且 Singleton 的构造函数已经对两个线程完成了执行。我会在我的答案中评论这一点。 - Bruno ReisgetInstance()
方法同步会引入一些同步开销。这可以通过“双重检查锁定”来大幅减少 - 请查看我的回答中的链接。 - Thomasvolatile
;如果不是volatile
,那么它必然是有问题的。 - Bruno Reis