2

Wikipedia上列出了 Java 中 Singleton 的一种实现:

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;

    private SingletonDemo() {
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

Java语言规范 17,第 5 段指出

当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象的最终字段的正确初始化值。

好的,假设我们的 SingletonDemo 类有非 final 字段。那么,并发线程将能够读取默认值而不是构造函数中指定的正确值吗?

4

4 回答 4

5

在 Java 5 及更高版本中可以正确实现双重检查锁定 (DCL)。在 Java 4 和更早版本中,这是不可能的,因为volatile没有正确指定与同步相关的行为(实际上是不充分的)。

您在问题中包含的代码是 DCL 的正确实现......当使用 Java 5 JRE 或更高版本运行时。

但是(IMO),不值得使用 DCL。特别是如果您(或追随您的开发人员)不完全了解如何正确/安全地执行此操作。

性能优势太小,无法在现实生活中的 Java 应用程序中进行有价值的优化。(如果是这样,你可能过度使用/误用单例......这会以其他方式咬你!)


好的,假设我们的 SingletonDemo 类有非 final 字段。那么,并发线程将能够读取默认值而不是构造函数中指定的正确值吗?

(引用的 JLS 文本是关于完全不同的情况。它是关于final字段的。这里不相关。而且你不能从不final同步的字段的行为中推断非同步字段的行为final。)

您的问题的答案是否定的。您问题中的代码足以保证并发线程不会看到默认值。要了解原因,请阅读以下内容:

  • JLS 第 17.4 节的所有内容,以及
  • Goetz et al "Java Concurrency in Practice" 的最后一章,其中包括关于 DCL 的部分(如果我没记错的话......)
于 2014-01-27T06:25:08.037 回答
4

你的报价说:

如果最终字段和构造函数完成,那么线程可以看到初始化值。

没有说:

如果非最终字段 THEN 线程看不到初始化值。

volatile 的语义也保证了该示例中的安全发布。

您还说 DCL 非常有用:我想说有更好的方法不需要在几乎所有情况下使用那种复杂且容易出错的构造。按优先顺序:

  • 根本不要使用单例
  • 使用枚举
  • 使用按需初始化持有人成语
于 2014-01-27T07:27:50.920 回答
0

并发线程甚至可以看到对象的最终字段处于未初始化状态。从您的代码中删除 volatile ,它可能会发生。在没有同步的情况下,只有创建对象的线程才能保证只有在构造完成初始化后才返回对该对象的引用。

于 2014-01-27T06:25:41.817 回答
0

引用您的维基百科链接,这将是最正确的双重检查易失性解决方案:

// 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%”。

或者,您可以使用Initialization-on-demand holder idiom

public class Something {
    private Something() {}

    private static class LazyHolder {
        private static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}

之所以有效,是因为在类加载期间,LazyHolderINSTANCE在实际访问类之前不会初始化,这是在getInstance()方法期间。

或者只是停止进行过于复杂的初始化,并使用标准的急切初始化:

static final Singleton INSTANCE = new Singleton();

只有极少数情况下,延迟初始化确实有帮助。也就是说,如果您的 Singleton 具有较高的初始化成本,并且在正常程序执行期间可能根本不会使用。但是这种情况通常是一个架构问题,延迟初始化只是一个快速而肮脏的修复。

所以你应该使用急切的初始化,除非你分析了你的应用程序并确定这是一个非常负面的性能问题。但是还有许多其他方法可以在架构级别上解决这个问题,而不是使用延迟初始化。

于 2014-01-27T13:20:45.493 回答