11

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

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

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

4

11 回答 11

13

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

不完全的。它是在第一次调用SingletonHolder初始化时构建的。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

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

于 2011-07-07T06:38:29.553 回答
3

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

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

于 2011-07-07T06:39:10.203 回答
1

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

于 2011-07-07T06:45:42.673 回答
1

您描述的模式有两个原因

  1. 首次访问时加载和初始化类(通过 SingletonHolder.INSTANCE 此处)
  2. 类加载和初始化在 Java 中是原子的

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

于 2011-07-07T06:47:38.470 回答
0

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

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

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

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

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

于 2011-07-07T06:42:34.523 回答
0

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

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

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

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

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

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

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

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

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

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

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

于 2011-07-07T07:06:56.940 回答
0

Java 中唯一正确的单音不能由类声明,而是由枚举声明:

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

用途如下:

Singleton reference1ToSingleton=Singleton.INST;    

所有其他方式不排除通过反射重复实例化或如果类的源直接存在于应用程序源中。枚举排除一切。(Enum 中的最终克隆方法确保永远无法克隆枚举常量

于 2012-02-01T11:37:13.417 回答
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 必须存在。

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

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

于 2011-07-14T08:10:36.637 回答
0

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

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

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

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

于 2011-07-07T06:45:07.627 回答
0

您急切地初始化,因为您不必编写同步块或方法。这主要是因为同步通常被认为是昂贵的

于 2011-07-07T06:35:26.047 回答
0

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

这里的“访问”是指规范中指定的有限操作。下一节将讨论初始化。

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

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

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

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

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

于 2011-07-07T06:50:21.037 回答