在Java中使用基于双重检查锁定的单例是否安全?
下面列出了一种Java中的Singleton实现:在Java中使用基于双重检查锁定的单例是否安全?,java,multithreading,thread-safety,final,java-memory-model,Java,Multithreading,Thread Safety,Final,Java Memory Model,下面列出了一种Java中的Singleton实现: 公共类单例演示{ 私有静态易失性SingletonDemo实例=null; 私有单音演示(){ } 公共静态SingletonDemo getInstance(){ if(实例==null){ 同步(SingletonDemo.class){ if(实例==null){ instance=new SingletonDemo(); } } } 返回实例; } } 有人说 当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后
公共类单例演示{
私有静态易失性SingletonDemo实例=null;
私有单音演示(){
}
公共静态SingletonDemo getInstance(){
if(实例==null){
同步(SingletonDemo.class){
if(实例==null){
instance=new SingletonDemo();
}
}
}
返回实例;
}
}
有人说
当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到该对象引用的线程才能确保看到该对象最终字段的正确初始化值
好,假设我们的SingletonDemo类有非final字段。因此,并发线程将能够读取默认值而不是构造函数中指定的正确值?在Java 5和更高版本中可以正确地实现双重检查锁定(DCL)。在Java4和更早版本中,这是不可能的,因为没有正确指定
volatile
在同步方面的行为(实际上是不充分的)
问题中包含的代码是DCL的正确实现。。。当使用Java5JRE或更高版本运行时
但是(IMO),不值得使用DCL。尤其是如果您(或您之后的开发人员)不完全了解如何正确/安全地执行此操作
对于实际Java应用程序来说,性能优势太小,不值得进行优化。(如果是这样的话,你很可能过度使用/误用了单例……这会在其他方面对你造成伤害!)
好,假设我们的SingletonDemo类有非final字段。所以,并发线程将能够读取默认值,而不是构造函数中指定的正确值 (引用的JLS文本是关于一种完全不同的情况。它是关于
final
字段的。它在这里不相关。并且,您无法从没有同步的final
字段的行为推断出有同步的非final
字段的行为。)
问题的答案是否定的。问题中的代码足以保证并发线程不会看到默认值。要了解原因,请阅读以下内容:
- JLS第17.4节的所有内容,以及
- Goetz等人的最后一章“实践中的Java并发”,其中包括一节关于DCL的内容(如果我没记错的话…)
- 一点也不要使用单件
- 使用枚举
- 使用按需初始化持有者惯用语
- 并发线程甚至可以在未初始化状态下看到对象的最终字段。从代码中删除volatile可能会发生这种情况。在缺少同步的情况下,只有创建对象的线程才能保证只有在构造完成初始化后才返回对对象的引用 你的报价是:
若最终字段和构造函数完成,那个么线程可以看到初始化的值
它不是说:
若为非最终字段,则线程无法看到初始化值
volatile的语义也保证了该示例中的安全发布
您还说DCL非常有用:我想说,有一些更好的方法,几乎在所有情况下都不需要使用这种复杂且容易出错的构造。按优先顺序:
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
private volatile Helper helper;
public Helper getHelper() {
Helper result = helper;
if (result == null) {
synchronized(this) {
result = helper;
if (result == null) {
helper = result = new Helper();
}
}
}
return result;
}
}
因为“volatile字段只被访问一次,这可以将方法的总体性能提高25%”
或者,您可以使用:
这是因为在类加载期间,LazyHolder实例在类实际被访问之前不会初始化,这是在getInstance()
方法期间
或者,无特殊原因停止执行复杂的初始化,使用标准的“急切初始化”:
static final Singleton INSTANCE = new Singleton();
只有极少数情况下,延迟初始化确实有帮助。也就是说,如果您的单例具有很高的初始化成本,并且在正常的程序执行期间,很可能根本不使用。但这种情况通常是一个架构问题,延迟初始化只是一个快速而肮脏的解决方案
因此,除非您分析了应用程序并确定这是一个严重的负面性能问题,否则您应该使用急切初始化。但是,在体系结构级别上,还有许多其他方法可以解决这个问题,而不是使用延迟初始化。双重检查锁定的答案不总是“不要使用双重检查锁定”吗?要么它不起作用,要么它没有帮助。@user2357112是真的,但这是一个有趣的问题。我认为无论如何都应该回答这个问题。不,DCL通常是非常有用的,你应该小心使用它。无论如何,这是许多资料中列出的一个常见例子。所以我只想确定它是否真的安全。这不是关于Singleton或DCL,它只是关于Java内存模型和线程模型。它与您的问题非常相关,因为它准确地解释了为什么它以前不起作用,为什么现在起作用。@WolframNyaa该链接是一个深入的解释,回答了您的问题。它是基于当前的内存模型编写的,这种模型已经好几年没有改变了。你应该读它(直到最后)…好的,但这是显而易见的。对非易失性变量的读/写不受之前发生的事件的限制,所以问题不在于此。
static final Singleton INSTANCE = new Singleton();