在 Java 中编写单例的不同方法

2022-09-04 23:52:57

用java编写单例的经典是这样的:

public class SingletonObject
{
    private SingletonObject()
    {
    }

    public static SingletonObject getSingletonObject()
    {
      if (ref == null)
          // it's ok, we can call this constructor
          ref = new SingletonObject();
      return ref;
    }

    private static SingletonObject ref;
}

如果需要在多线程情况下运行,我们可以添加同步关键字。

但我更喜欢把它写成:

public class SingletonObject
{
    private SingletonObject()
    {
        // no code req'd
    }

    public static SingletonObject getSingletonObject()
    {
      return ref;
    }

    private static SingletonObject ref = new SingletonObject();
}

我认为这更简洁,但奇怪的是,我没有看到任何以这种方式编写的示例代码,如果我以这种方式编写代码,是否有任何不良影响?


答案 1

代码与“示例代码”之间的区别在于,在加载类时会实例化单例,而在“示例”版本中,直到实际需要它才会实例化。


答案 2

在第二种形式中,您的单例被紧急加载,这实际上是首选形式(正如您自己提到的,第一种形式不是线程安全的)。对于生产代码来说,急切加载并不是一件坏事,但是在有些上下文中,您可能希望延迟加载单例,正如Guice的作者Bob Lee在延迟加载单例中所讨论的那样,我在下面引用:

首先,为什么要延迟加载单例?在生产环境中,您通常希望热切地加载所有单例,以便尽早发现错误并预先解决任何性能问题,但在测试和开发过程中,您只想加载绝对需要的内容,以免浪费时间。

在Java 1.5之前,我使用普通的旧同步来延迟加载单例,简单但有效:

static Singleton instance;

public static synchronized Singleton getInstance() {
  if (instance == null)
    instance = new Singleton();
  return instance;
}

在 1.5 中对内存模型的更改启用了臭名昭著的双重检查锁定 (DCL) 习语。要实现 DCL,请检查公共路径中的字段,并仅在必要时进行同步:volatile

static volatile Singleton instance;

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

但是,现在的速度不是很快吗,DCL需要更多的代码,所以即使在1.5出来之后,我仍然继续使用普通的旧同步。volatilesynchronizedsynchronized

想象一下,当Jeremy Manson向我指出初始化按需持有者(IODH)成语时,我今天感到惊讶,该习语需要很少的代码并且同步开销为零。零,甚至比 .IODH 需要与普通旧同步相同的代码行数,并且比 DCL 更快!volatile

IODH 利用惰性类初始化。JVM 不会执行类的静态初始值设定项,直到您实际接触类中的某些内容。这也适用于静态嵌套类。在下面的示例中,JLS 保证在有人调用之前 JVM 不会初始化:instancegetInstance()

static class SingletonHolder {
  static Singleton instance = new Singleton();    
}

public static Singleton getInstance() {
  return SingletonHolder.instance;
}

[...]

更新:在应得的信贷中,Effective Java(版权2001)在第48项下详细描述了这一模式。它继续指出,您仍然必须在非静态上下文中使用同步或 DCL。

我还将框架中的单例处理从同步切换到 DCL,并看到了另一个 10% 的性能提升(与我开始使用 cglib 的快速反射之前相比)。我在我的微基准测试中只使用了一个线程,因此,鉴于我用相对细粒度的易失性字段访问替换了备受争议的锁,因此对并发性的提升可能会更大。

请注意,Joshua Bloch现在建议(自 Effective Java 以来,第 2 版)使用 Jonik 指出的单元素实现单例。enum


推荐