【问题标题】:Does Java have the static order initialisation fiasco?Java有静态订单初始化惨败吗?
【发布时间】:2011-07-07 06:31:41
【问题描述】:

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

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真的这个问题吗?我知道一个类的顺序 within 是完全指定的,但它是否能以某种方式保证类之间的顺序一致(例如使用类加载器)?

其次,如果顺序 是一致的,为什么惰性构造选项会有优势?

【问题讨论】:

    标签: java singleton static-order-fiasco


    【解决方案1】:

    现在,我想明白这是在做什么了。由于实例是静态最终的,它早在任何线程调用 getInstance() 之前就已构建,因此实际上不需要同步。

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

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

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

    考虑

    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
    

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

    【讨论】:

    • 我认为延迟初始化的普遍原因是将一些(可能)昂贵的东西推迟到绝对需要的时候,希望它根本不需要。如果您的应用程序运行时间超过 x,则假设您的应用程序想要下载某些东西。用户可以在 x 之前关闭它,在这种情况下(通常是昂贵的)连接创建将是一种浪费。
    • @Michal,如果您像在 OP 中那样在加载小型帮助程序类时进行计算,那么 Java 的延迟加载将为您提供延迟初始化的所有好处。如果它不应该是全球性的w.r.t。一个类加载器,那么这不是一个选项。
    【解决方案2】:

    现在,我想明白这是什么了 正在做。由于实例是静态的 最终,它早在任何时候就建成了 线程将调用 getInstance() 所以 没有真正的需要 同步。

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        您描述的模式有两个原因

        1. 类在第一次访问时被加载和初始化(通过 SingletonHolder.INSTANCE 这里)
        2. 类加载和初始化在 Java 中是原子的

        因此,您确实以线程安全且高效的方式执行延迟初始化。这种模式是同步惰性初始化的双锁(不工作)解决方案的更好替代方案。

        【讨论】:

          【解决方案5】:

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

          【讨论】:

            【解决方案6】:

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

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

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

            第二个调用者将永远看到问题的根本原因(他只会得到NoClassDefFoundError)。因此,如果第一个调用者以某种方式忽略了问题,那么您将永远无法找出究竟是什么出了问题。

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

            【讨论】:

              【解决方案7】:

              一个类在运行时被访问时被初始化。所以初始化顺序几乎就是执行顺序。

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

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

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

              (一旦初始化,同步块就没有用了;JVM会优化掉它。)

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

              由于它搭载类初始化语义,它仅适用于静态实例。一般来说,惰性求值并不局限于静态实例;假设每个会话都有一个实例。

              【讨论】:

                【解决方案8】:

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

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

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

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

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

                其次,如果顺序是一致的,为什么惰性构造选项会有优势?

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

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

                • 当初始化成本很高,并且您不希望它浪费时间做不必要的事情时......或者您希望更重要的事情尽快发生(例如显示 UI)。

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

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

                【讨论】:

                  【解决方案9】:

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

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

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

                  已编辑
                  版本 2 是线程安全的,但有点贵,更重要的是,严重限制了并发/吞吐量

                  【讨论】:

                  • 也许我不明白,但我认为版本 2 也是线程安全的:public static synchronized Singleton getInstance()。它只是有每次访问同步的开销。
                  • 对不起,我的错 - 我错过了同步方法 - 你们都是正确的,它是安全的。 +1 给你们每个人 :)
                  【解决方案10】:

                  我不喜欢你的代码 sn-p,但我有你的问题的答案。是的,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 必须存在。

                  我的实际用例有点复杂——枚举之间的关系实际上是父子关系,所以一个枚举返回对其父级的引用,但它的第二个子数组。孩子是枚举的私有静态字段。有趣的是,在 Windows 上开发时一切正常,但在生产环境(即 Solaris)中,子数组的成员为空。该数组具有适当的大小,但其元素为空,因为在实例化数组时它们不可用。

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

                  【讨论】:

                    【解决方案11】:

                    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)

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2011-07-15
                      • 1970-01-01
                      • 1970-01-01
                      • 2011-01-23
                      相关资源
                      最近更新 更多