Java有静态订单初始化惨败吗?

时间:2011-07-07 06:31:42

标签: java singleton static-order-fiasco

这里最近的一个问题有以下代码(好吧,类似于这个)来实现没有同步的单例。

public class Singleton {
    private Singleton() {}
    private static class SingletonHolder { 
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

现在,我想了解这是做什么的。由于实例是static final,因此在任何线程调用getInstance()之前很久就会构建它,因此不需要同步。

只有当两个线程同时尝试调用getInstance()时才需要进行同步(并且该方法在第一次调用时而不是在"static final"时进行构建)。

因此,基本上我的问题是:为什么你会喜欢单身的懒惰建构:

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

我唯一的想法是使用static final方法可能会引入排序问题,如C ++静态初始化顺序惨败。

首先,Java实际上这个问题吗?我知道中的命令是完全指定的,但它是否能保证类之间的顺序(例如使用类加载器)?

其次,如果订单 一致,那么为什么延迟构造选项会有利?

11 个答案:

答案 0 :(得分:13)

  

现在,我想了解这是做什么的。由于实例是静态final,所以在任何线程调用getInstance()之前很久就会构建它,因此不需要同步。

不完全。它是在SingletonHolder类为initialized时构建的,这是第一次调用getInstance时发生的。类加载器具有单独的锁定机制,但在加载类之后,不需要进一步锁定,因此该方案只进行了足够的锁定以防止多次实例化。

  

首先,Java确实存在这个问题吗?我知道类中的顺序是完全指定的,但它是否以某种方式保证了类之间的一致顺序(例如使用类加载器)?

Java确实存在一个问题,即类初始化周期可能会导致某些类在初始化之前观察另一个类的静态终结(技术上是在所有静态初始化程序块运行之前)。

考虑

class A {
  static final int X = B.Y;
  // Call to Math.min defeats constant inlining
  static final int Y = Math.min(42, 43);
}

class B {
  static final int X = A.Y;
  static final int Y = Math.min(42, 43);
}

public class C {
  public static void main(String[] argv) {
    System.err.println("A.X=" + A.X + ", A.Y=" + A.Y);
    System.err.println("B.X=" + B.X + ", B.Y=" + B.Y);
  }
}

运行C打印

A.X=42, A.Y=42
B.X=0, B.Y=42

但是在你发布的成语中,助手和单例之间没有循环,因此没有理由更喜欢延迟初始化。

答案 1 :(得分:3)

  

现在,我想了解这是什么   这样做。由于实例是静态的   最终,它早在任何之前就建成了   线程会调用getInstance()   没有必要   同步。

没有。只有在您第一次调用SingletonHolder时才会加载SingletonHolder.INSTANCE类。 final对象只有在完全构造后才会对其他线程可见。这种延迟初始化称为Initialization on demand holder idiom

答案 2 :(得分:1)

Effective Java中,Joshua Bloch指出“这idiom ...利用了保证在使用[JLS, 12.4.1]之前不会初始化类。”

答案 3 :(得分:1)

你描述的模式有两个原因

  1. 首次访问时加载并初始化类(通过SingletonHolder.INSTANCE此处)
  2. 类加载和初始化在Java中是原子的
  3. 所以你确实以线程安全有效的方式执行延迟初始化。这种模式可以替代双锁(不工作)解决方案来同步延迟初始化。

答案 4 :(得分:0)

您急切地初始化,因为您不必编写同步块或方法。这主要是因为同步是generally considered expensive

答案 5 :(得分:0)

关于第一个实现的一点注意事项:这里有趣的是类初始化用于替换经典同步。

类初始化的定义非常明确,除非完全初始化(即所有静态初始化代码都已运行),否则任何代码都无法访问该类的任何内容。并且由于可以使用大约零开销来访问已经加载的类,这会将“同步”开销限制为需要进行实际检查的情况(即“是否已加载/初始化类?”)。

使用类加载机制的一个缺点是,它在中断时很难调试。如果由于某种原因,Singleton构造函数抛出异常,那么第一个调用者将getInstance()将获得该异常(包含在另一个异常中)。

第二个来电者永远不会查看问题的根本原因(他只会得到一个NoClassDefFoundError)。因此,如果第一个调用者以某种方式忽略了该问题,那么您永远不会能够找到究竟出错的地方。

如果你只使用同步,那么第二个被调用只会尝试再次实例化Singleton并且可能会遇到同样的问题(甚至成功!)。

答案 6 :(得分:0)

第一个版本中的代码是正确的最佳方式,可以安全地懒惰地构建单例。 Java内存模型保证INSTANCE将:

  • 仅在第一次实际使用时(即懒惰)进行初始化,因为类仅在首次使用时加载
  • 构造一次,因此它完全是线程安全的,因为所有静态初始化都保证在类可用之前完成

版本1是一个很好的模式。

<强> EDITED
版本2是线程安全的,但有点贵,更重要的是,严重限制了并发/吞吐量

答案 7 :(得分:0)

在运行时访问类时会初始化该类。所以init命令几乎就是执行顺序。

“访问”在这里指的是有限的行为specified in the spec。下一节将讨论初始化。

第一个例子中发生的事情是等效的

public static Singleton getSingleton()
{
    synchronized( SingletonHolder.class )
    {
        if( ! inited (SingletonHolder.class) )
            init( SingletonHolder.class );
    } 
    return SingletonHolder.INSTANCE;
}

(初始化后,同步块变得无用; JVM将对其进行优化。)

从语义上讲,这与第二个impl没有什么不同。这并没有真正超越“双重检查锁定”,因为双重检查锁定。

由于它背负着类init语义,它只适用于静态实例。通常,延迟评估不仅限于静态实例;想象每个会话都有一个实例。

答案 8 :(得分:0)

  

首先,Java确实存在这个问题吗?我知道类中的顺序是完全指定的,但它是否以某种方式保证了类之间的一致顺序(例如使用类加载器)?

确实如此,但程度低于C ++:

  • 如果没有依赖循环,静态初始化将按正确的顺序发生。

  • 如果在一组类的静态初始化中存在依赖循环,那么类的初始化顺序是不确定的。

  • 但是,Java保证在任何代码看到字段的值之前发生静态字段的默认初始化(为null / zero / false)。因此,无论初始化顺序如何,都可以(在理论上)编写一个类来做正确的事。

  

其次,如果订单是一致的,为什么懒惰的构造选项会有利?

延迟初始化在许多情况下都很有用:

  • 当初始化具有您不希望发生的副作用时,除非实际上将要使用该对象。

  • 当初始化很昂贵时,您不希望它浪费时间不必要地执行它...或者您希望更快地发生更重要的事情(例如显示UI)。

  • 初始化取决于静态初始化时不可用的某些状态。 (虽然你需要小心这一点,因为在延迟初始化被触发时状态可能不可用。)

您还可以使用同步getInstance()方法实现延迟初始化。它更容易理解,但它会使getInstance()稍微慢一些。

答案 9 :(得分:0)

我没有进入您的代码段,但我对您的问题有答案。是的,Java有一个初始化命令惨败。我遇到了相互依赖的枚举。一个例子如下:

enum A {
  A1(B.B1);
  private final B b;
  A(B b) { this.b = b; }
  B getB() { return b; }
}

enum B {
  B1(A.A1);
  private final A a;
  B(A a) { this.a = a; }
  A getA() { return a; }
}

关键是在创建实例A.A1时必须存在B.B1。并创建A.A1 B.B1必须存在。

我的真实用例有点复杂 - 枚举之间的关系实际上是父子关系,因此一个枚举返回对其父级的引用,但是其子级的第二个数组。孩子们是enum的私人静态领域。有趣的是,在Windows上进行开发时,一切工作正常,但在生产中 - 这是Solaris - 子数组的成员为空。该数组具有适当的大小,但其元素为null,因为它们在实例化数组时不可用。

所以我在第一次调用时结束了同步初始化。 : - )

答案 10 :(得分:0)

Java中唯一正确的单一声明可以不是按类声明,而是通过枚举声明:

public enum Singleton{
   INST;
   ... all other stuff from the class, including the private constructor
}

用法如下:

Singleton reference1ToSingleton=Singleton.INST;    

所有其他方法都不排除通过反射重复实例化,或者类源是否直接存在于应用程序源中。枚举不包括一切。 (The final clone method in Enum ensures that enum constants can never be cloned