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

gle*_*ery 12 java singleton

在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;
}
Run Code Online (Sandbox Code Playgroud)

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

但我更喜欢把它写成:

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

    public static SingletonObject getSingletonObject()
    {
      return ref;
    }

    private static SingletonObject ref = new SingletonObject();
}
Run Code Online (Sandbox Code Playgroud)

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

Ano*_*on. 18

您的代码与"示例代码"之间的区别在于,在加载类时会对您的单例进行实例化,而在"示例"版本中,在实际需要之前不会对其进行实例化.

  • 当然,差异通常是无关紧要的,因为java类是按需加载的. (5认同)

Pas*_*ent 18

在第二种形式中,你的单例被急切地加载,这实际上是首选形式(并且第一种形式不是你自己提到的线程安全的).渴望加载对于生产代码来说并不是一件坏事,但是有些情况下你可能想要懒惰加载你的单身,正如Guice的作者Bob Lee 所说,我在下面引用的Lazy Loading Singletons:

首先,为什么要延迟加载单例?在生产中,您通常需要急切地加载所有单例,以便及早发现错误并预先考虑任何性能,但在测试和开发过程中,您只想加载您绝对需要的内容,以免浪费时间.

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

static Singleton instance;

public static synchronized Singleton getInstance() {
  if (instance == null)
    instance == new Singleton();
  return instance;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

volatile速度并不快synchronized,synchronized现在相当快,DCL需要更多代码,所以即使在1.5出现之后,我继续使用普通的旧同步.

想象一下,今天我惊讶的是Jeremy Manson向我指出了 初始化按需持有者(IODH)习语,它需要很少的代码并且没有同步开销.零,比在更快volatile.IODH需要与普通旧同步相同数量的代码行,并且它比DCL快!

IODH使用惰性类初始化.在您真正触摸类中的某些内容之前,JVM不会执行类的静态初始化程序.这也适用于静态嵌套类.在以下示例中, JLS保证 JVM instance在有人调用之前不会初始化getInstance():

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

public static Singleton getInstance() {
  return SingletonHolder.instance;
}
Run Code Online (Sandbox Code Playgroud)

[...]

更新:信用到期的信用,Effective Java(版权所有2001)在第48项下详述了此模式.它继续指出您仍然必须在非静态上下文中使用同步或DCL.

我还在我的框架中将单例处理从同步切换到DCL,并且看到了另外10%的性能提升(与之前我开始使用cglib的快速反射相比).我只在我的微基准测试中使用了一个线程,因此我可以使用相对细粒度的易失性字段访问来替换具有相当细粒度的易失性锁定,从而提高并发性.

请注意,Joshua Bloch现在推荐(自Effective Java,第2版以来)使用Jonikenum指出的单元素实现单例.


Jon*_*nik 10

好吧,在后一种情况下,单例对象会在需要之前创建,但在大多数情况下,这可能不是非常糟糕.

顺便说一下,Joshua Bloch建议(在Effective Java,第2版,第3项中)使用单元素枚举实现单例:

public enum SingletonObject { 
    INSTANCE;
}
Run Code Online (Sandbox Code Playgroud)

他给出了以下理由:

[...]它更简洁,免费提供序列化机制,并提供防止多个实例化的铁定保证,即使面对复杂的序列化或反射攻击.虽然这种方法尚未被广泛采用,但单元素枚举类型是实现单例的最佳方法.


Jon*_*eet 5

我会说后一个代码实际上是更标准的模式.您的第一个版本不是线程安全的.使其成为线程安全的方法包括在每次访问时进行同步,或者非常小心地使用双重检查锁定(从Java 5内存模型开始就是安全的,只要你做对了).

请注意,由于类被懒惰地初始化,如果您在类上调用静态方法而不想创建实例,则后一个代码仍然只会不必要地创建一个对象.

有一个模式使用嵌套类进行初始化,这可以使这个更懒,但个人第二种形式几乎总是对我自己做得足够好.

在Effective Java中有更多详细信息,但是我没有找到项目编号,我很害怕.