在这方面可能有点晚了,但基本实现看起来像这样:
public class MySingleton {
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
...
}
这里我们有一个 MySingleton 类,它有一个名为 INSTANCE 的私有静态成员和一个名为 getInstance() 的公共静态方法。第一次调用 getInstance() 时,INSTANCE 成员为空。然后流程将落入创建条件并创建 MySingleton 类的新实例。对 getInstance() 的后续调用将发现 INSTANCE 变量已设置,因此不会创建另一个 MySingleton 实例。这确保只有一个 MySingleton 实例在 getInstance() 的所有调用者之间共享。
但是这个实现有一个问题。多线程应用程序在创建单个实例时会有竞争条件。如果多个执行线程同时(或大约)同时调用 getInstance() 方法,它们都会将 INSTANCE 成员视为 null。这将导致每个线程创建一个新的 MySingleton 实例并随后设置 INSTANCE 成员。
private static MySingleton INSTANCE;
public static synchronized MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
这里我们使用方法签名中的 synchronized 关键字来同步 getInstance() 方法。这肯定会解决我们的比赛条件。线程现在将阻塞并一次进入一个方法。但它也会产生性能问题。此实现不仅同步单个实例的创建,还同步所有对 getInstance() 的调用,包括读取。读取不需要同步,因为它们只是返回 INSTANCE 的值。由于读取将构成我们调用的大部分(请记住,实例化仅在第一次调用时发生),因此同步整个方法会导致不必要的性能损失。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronize(MySingleton.class) {
INSTANCE = new MySingleton();
}
}
return INSTANCE;
}
在这里,我们将同步从方法签名移到了一个同步块,该块包装了 MySingleton 实例的创建。但这能解决我们的问题吗?好吧,我们不再阻止读取,但我们也向后退了一步。多个线程将同时或大约同时访问 getInstance() 方法,它们都将 INSTANCE 成员视为 null。然后,他们将点击同步块,其中一个人将获得锁并创建实例。当该线程退出该块时,其他线程将争夺锁,每个线程将一个接一个地穿过该块并创建我们类的新实例。所以我们又回到了开始的地方。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
在这里,我们从 INSIDE 块发出另一个检查。如果已经设置了 INSTANCE 成员,我们将跳过初始化。这称为双重检查锁定。
这解决了我们的多重实例化问题。但是,我们的解决方案再一次提出了另一个挑战。其他线程可能不会“看到” INSTANCE 成员已更新。这是因为 Java 如何优化内存操作。线程将变量的原始值从主存复制到 CPU 的缓存中。然后将值的更改写入该缓存并从该缓存中读取。这是 Java 的一个特性,旨在优化性能。但这给我们的单例实现带来了问题。第二个线程 — 由不同的 CPU 或内核处理,使用不同的缓存 — 不会看到第一个线程所做的更改。这将导致第二个线程将 INSTANCE 成员视为 null,从而强制创建我们的单例的新实例。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
我们通过在 INSTANCE 成员的声明中使用 volatile 关键字来解决这个问题。这将告诉编译器始终读取和写入主内存,而不是 CPU 缓存。
但这种简单的改变是有代价的。因为我们绕过了 CPU 缓存,所以每次操作 volatile INSTANCE 成员时都会对性能造成影响 — 我们做了 4 次。我们再次检查存在(1 和 2),设置值(3),然后返回值(4)。有人可能会说这条路径是边缘情况,因为我们只在第一次调用该方法时创建实例。也许对创作的影响是可以容忍的。但即使是我们的主要用例 reads 也会对 volatile 成员进行两次操作。一次检查存在,再次返回其值。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
MySingleton result = INSTANCE;
if (result == null) {
synchronized(MySingleton.class) {
result = INSTANCE;
if (result == null) {
INSTANCE = result = createInstance();
}
}
}
return result;
}
由于性能损失是由于直接对 volatile 成员进行操作,让我们将一个局部变量设置为 volatile 的值,并改为对局部变量进行操作。这将减少我们对 volatile 进行操作的次数,从而收回我们失去的一些性能。请注意,当我们进入同步块时,我们必须再次设置我们的局部变量。这样可以确保在我们等待锁定时发生的任何更改都是最新的。
我最近写了一篇关于这个的文章。 Deconstructing The Singleton。您可以在此处找到有关这些示例的更多信息以及“持有人”模式的示例。还有一个真实的例子展示了双重检查的易失性方法。希望这会有所帮助。