Java:惰性初始化单例

Java:惰性初始化单例,java,design-patterns,synchronization,singleton,Java,Design Patterns,Synchronization,Singleton,创建单例的模式似乎类似于: public class Singleton { private static final Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance() { return instance; } } 然而,我的问题是,如果单例构造函数做了一些不利于单元测试的事情,例如

创建单例的模式似乎类似于:

public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton(){
    }

    public static Singleton getInstance()
    {
        return instance;
    }
}
然而,我的问题是,如果单例构造函数做了一些不利于单元测试的事情,例如调用外部服务、jndi查找等,那么如何使用这样的类进行单元化呢

我想我可以像这样重构它:

public class Singleton {
    private static Singleton instance;
    private Singleton(){
    }

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

     //for the unit tests
     public static void setInstance(Singleton s)
     {
          instancce = s;
     }
}
现在的问题是,仅仅为了单元可测试性,我已经强制getInstance同步,所以仅仅为了测试方面,它将对实际应用程序产生负面影响。有没有办法解决这个问题,似乎任何其他类型的延迟初始化都不起作用,因为java中双重锁定模式的性质已被破坏。

您可以使用创建单例,并根据环境切换实现


或者,避免使用单例模式,而是使用。

您可以将枚举用作单例

enum Singleton {
    INSTANCE;
}
假设您的单例在单元测试中做了一些不受欢迎的事情,您可以

// in the unit test before using the Singleton, or any other global flag.
System.setProperty("unit.testing", "true");

Singleton.INSTANCE.doSomething();

enum Singleton {
    INSTANCE;
    {
        if(Boolean.getBoolean("unit.testing")) {
           // is unit testing.
        } else {
           // normal operation.
        }
    }
}
注意:不需要同步块或显式锁。在访问.class之前不会加载实例,在使用成员之前不会初始化实例。如果只使用
Singleton.INSTANCE
而不使用
Singleton.class
,则用于初始化的值在以后更改时不会出现问题


编辑:如果只使用
Singleton.class
,则可能不会初始化该类。在本例中,在Java8Update112上没有这样做

public class ClassInitMain {
    public static void main(String[] args) {
        System.out.println("Printing a class reference");
        Class clazz = Singleton.class;
        System.out.println("clazz = " + clazz);
        System.out.println("\nUsing an enum value");
        Singleton instance = Singleton.INSTANCE;
    }

    static enum Singleton {
        INSTANCE;

        Singleton() {
            System.out.println(getClass() + " initialised");
        }
    }
}
印刷品

Printing a class reference
clazz = class ClassInitMain$Singleton

Using an enum value
class ClassInitMain$Singleton initialised

您可以依赖注入singleton实例,重写单元测试代码中的getInstance(),使用面向方面的编程来拦截方法调用并返回不同的对象,或者使用类似的工具来模拟几乎任何东西,包括静态、最终类、构造函数以及人们通常说的所有东西“不稳定。”

我在遗留系统中采用的一种方法是修改工厂方法(getInstance)检查系统属性是否有我将实例化的替代实现。这被设置为单元测试套件中的替代模拟对象


至于“double checked locking is breake”语句,如果您使用volatile关键字,并且Java>=1.5,则该语句已被破坏(即使使用volatile)使用1.4和更早版本,但如果您知道您的代码将只在最近的JVM上运行,我不会担心。但我也不会使用单例:让DI/IOC容器管理对象的生命周期将解决这两个问题(可测试性和同步访问器瓶颈)更优雅。

在执行单元测试的构建阶段延迟初始化如何。然后在编译分发之前将代码更改回内联初始化

您的生产代码是内联初始化的,测试期间除外。顺便说一句,生产代码和测试代码之间的这种差异可能会引入bug,但哪一个呢


(当然,如果这是一个解决方案,我们让一个构建阶段+工具来完成这项工作。我认为这在maven和中得到了促进)。

在每种语言中都打破了双重检查锁定,而不仅仅是Java

我倾向于避免使用单例,但如果需要,可以使用holder模式,正如Josh Bloch的《高效Java:


编辑:修复了引用。

hmm我喜欢系统属性的想法,它可以标记单元测试。理想情况下,最好不使用单例,但它比我高一点,旧代码库大,没有时间重新考虑:(.您已经发现了Singleton模式的一个关键缺点:它将您锁定在Singleton接口的特定实现中。是的,您可以通过各种方式解决这个问题,最糟糕的是。相反,问问自己,您是否真的需要一个Singleton。为什么您需要惰性初始化,而不是Java已经自动提供的东西s表示每种类型?即使通过引用
.class
,实例也会加载,但不会初始化。@LewBloch可能有JVM发生这种情况,但Oracle JVM 8 update 112不正确。JLS解释说。类型加载到引用
.class
文本上,很明显,否则文本不会在内存中o首先引用。这将加载类型,但不会初始化它。您可能混淆了加载和初始化。引用
.class
本身就足以加载类型,但根据JLS的说法,至少返回到Java 1.4,不应该触发类型初始化。在Java 1.5之前,Sun JVM有一个bug,其中此类引用实际上触发了类型初始化。他们在Java 5中修复了该bug,从那时起,该功能就按照规范@LewBloch hmmm运行。我承认,我对它进行了重新编写,因为它令人困惑。尽管我提到了实现单例的holder类解决方案,但这几乎总是过于工程化。simp正如@Peter Lawrey所指出的,最可靠且几乎总是正确的方法是使用单个常量定义枚举。
public class Foo
{
  static class Holder
  {
    static final Foo instance = new Foo();
  }

  public static Foo getInstance()
  {
    return Holder.instance;
  }

  private Foo()
  {
  }

  // ...
}