从 Head First 设计模式书中,具有双重检查锁定的单例模式已实现如下:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
我不明白为什么使用volatile
。 volatile
使用不会破坏使用双重检查锁定的目的,即性能?
答案 0 :(得分:58)
理解为什么需要volatile
的良好资源来自JCIP一书。维基百科也有decent explanation个材料。
真正的问题是Thread A
可能会在instance
构建instance
之前为Thread B
分配一个内存空间。 Thread B
会看到该分配并尝试使用它。这会导致instance
失败,因为它使用的是部分构造版本的{{1}}。
答案 1 :(得分:16)
正如@irreputable引用的那样,不稳定并不昂贵。即使价格昂贵,也应优先考虑性能。
Lazy Singletons还有一个更优雅的方式。
public final class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
来源文章:来自维基百科的Initialization-on-demand_holder_idiom
在软件工程中,Initialization on Demand Holder(设计模式)习语是一个懒惰的单例。在所有版本的Java中,这个习惯用法都能实现安全,高度并发的延迟初始化和良好的性能
由于该类没有要初始化的任何static
变量,因此初始化很简单。
在JVM确定必须执行LazyHolder之前,不会初始化其中的静态类定义LazyHolder
。
静态类LazyHolder
仅在类Singleton上调用静态方法getInstance
时执行,并且第一次发生这种情况时,JVM将加载并初始化LazyHolder
类。
此解决方案是线程安全的,无需特殊的语言结构(即volatile
或synchronized
)。
答案 2 :(得分:8)
嗯,没有双重检查锁定的性能。这是一个破碎的模式。
抛开情绪,volatile
就在这里因为没有它,当第二个线程通过instance == null
时,第一个线程可能还没有构造new Singleton()
:没有人承诺创建对象< em>在为instance
分配给任何线程之前发生,但实际创建对象的线程。
volatile
反过来在读取和写入之间建立发生之前的关系,并修复损坏的模式。
如果您正在寻找性能,请改用holder inner static class。
答案 3 :(得分:2)
如果你没有它,第二个线程可能会在第一个线程设置为null之后进入synchronized块,而你的本地缓存仍然认为它是null。
第一个不是为了正确(如果你是正确的,那将是自我失败),而是为了优化。
答案 4 :(得分:1)
易失性读取本身并不昂贵。
您可以设计一个测试来在紧密循环中调用getInstance()
,以观察易失性读取的影响;但是那个测试是不现实的;在这种情况下,程序员通常会调用getInstance()
一次并在使用期间缓存实例。
另一个意思是使用final
字段(请参阅维基百科)。这需要额外的读取,这可能比volatile
版本更昂贵。 final
版本在紧密循环中可能更快,但是该测试没有像之前所说的那样有用。
答案 5 :(得分:0)
将变量声明为volatile
可确保对它的所有访问实际上都从内存中读取其当前值。
如果没有volatile
,编译器可以优化存储器访问并将其值保存在寄存器中,因此只有第一次使用变量才能读取保存变量的实际存储器位置。如果变量被第一次和第二次访问之间的另一个线程修改,则会出现问题。第一个线程只有第一个(预修改的)值的副本,因此第二个if
语句测试变量值的陈旧副本。
答案 6 :(得分:0)
双重检查锁定是一种防止在多线程环境中调用getInstance
方法时创建另一个单例实例的技术。
volatile
关键字,用于实例成员的声明。这将告诉编译器始终从主内存而不是从CPU高速缓存读取和写入。使用volatile
变量保证事前发生关系,所有写操作都将在实例变量的任何读操作之前发生。volatile
关键字才能正常工作,因此它与Java 1.4及更低版本不兼容。问题在于,无序写操作可能允许实例引用在执行单例构造函数之前被返回。每个人的详细说明都太冗长,因此我只链接了一篇好文章-All you want to know about Singleton