Java 是否存在静态顺序初始化失败的情况?

发布于 2024-11-19 00:04:05 字数 1067 浏览 1 评论 0原文

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

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

现在,我想明白这是在做什么了。由于该实例是staticfinal,它是在任何线程调用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;
    }
}

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

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

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

A recent question here had the following code (well, similar to this) to implement a singleton without synchronisation.

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

Now, I think understand what this is doing. Since the instance is static final, it's built long before any threads will call getInstance() so there's no real need for synchronisation.

Synchronisation would be needed only if two threads tried to call getInstance() at the same time (and that method did construction on first call rather than at "static final" time).

My question is therefore basically: why then would you ever prefer lazy construction of the singleton with something like:

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

My only thoughts were that using the static final method may introduce sequencing issue as in the C++ static initialisation order fiasco.

First off, does Java actually have this problem? I know order within a class is fully specified but does it somehow guarantee consistent order between classes (such as with a class loader)?

Secondly, if the order is consistent, why would the lazy construction option ever be advantageous?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

蒗幽 2024-11-26 00:04:05

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

不完全是。它是在 SingletonHolder 类为

首先,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

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

Now, I think understand what this is doing. Since the instance is static final, it's built long before any threads will call getInstance() so there's no real need for synchronisation.

Not quite. It is built when the SingletonHolder class is initialized which happens the first time getInstance is called. The classloader has a separate locking mechanism, but after a class is loaded, no further locking is needed so this scheme does just enough locking to prevent multiple instantiation.

First off, does Java actually have this problem? I know order within a class is fully specified but does it somehow guarantee consistent order between classes (such as with a class loader)?

Java does have a problem where a class initialization cycle can lead to some class observing another class's static finals before they are initialized (technically before all static initializer blocks have run).

Consider

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);
  }
}

Running C prints

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

But in the idiom you posted, there is no cycle between the helper and the singleton so there is no reason to prefer lazy initialization.

假装不在乎 2024-11-26 00:04:05

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

不会。仅当您第一次调用 SingletonHolder.INSTANCE 时才会加载 SingletonHolder 类。 final 对象只有在完全构造完成后才会对其他线程可见。这种惰性初始化称为按需持有者初始化习惯用法

Now, I think understand what this is
doing. Since the instance is static
final, it's built long before any
threads will call getInstance() so
there's no real need for
synchronisation.

No. SingletonHolder class will be loaded only when you invoke SingletonHolder.INSTANCE for the very first time. final Object will become visible to other threads only after it is fully constructed. Such lazy initialization is called Initialization on demand holder idiom.

北方的韩爷 2024-11-26 00:04:05

Effective Java 中,Joshua Bloch 指出“这 < a href="http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom" rel="nofollow">惯用语 …利用类在使用之前不会被初始化的保证 [JLS,12.4.1]。”

In Effective Java, Joshua Bloch notes that "This idiom … exploits the guarantee that a class will not be initialized until it is used [JLS, 12.4.1]."

梦幻之岛 2024-11-26 00:04:05

您所描述的模式有两个原因:

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

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

The patern that you described works for two reasons

  1. Class is loaded and initialized when first accessed (via SingletonHolder.INSTANCE here)
  2. Class loading and initialization is atomic in Java

So you do perform lazy initialization in a thread safe and efficient way. This pattern is better alternative to double lock (not working) solution to synchronized lazy init.

樱花细雨 2024-11-26 00:04:05

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

You initialize eagerly because you don't have to write a synchronized block or method. This is mainly because synchronization is generally considered expensive

甜妞爱困 2024-11-26 00:04:05

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

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

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

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

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

Just a little note about the first implementation: the interesting thing here is that class initialization is used to replace classical synchronization.

Class initialization is very well defined in that no code can ever get access to anything of the class unless it is fully initialized (i.e. all static initializer code has run). And since an already loaded class can be accessed with about zero overhead, this restricts the "synchronization" overhead to those cases where there is an actual check to be done (i.e. "is the class loaded/initialized yet?").

One drawback of using the class loading mechanism is that it can be hard to debug when it breaks. If, for some reason, the Singleton constructor throws an exception, then the first caller to getInstance() will get that exception (wrapped in another one).

The second caller however will never see the root cause of the problem (he will simply get a NoClassDefFoundError). So if the first caller somehow ignores the problem, then you'll never be able to find out what exactly went wrong.

If you use simply synchronization, then the second called will just try to instantiate the Singleton again and will probably run into the same problem (or even succeed!).

嗳卜坏 2024-11-26 00:04:05

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

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

第一个示例中发生的情况是等效的

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

(一旦初始化,同步块就变得无用;JVM 将优化它。)

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

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

A class is initialized when it's accessed at runtime. So init order is pretty much the execution order.

"Access" here refers to limited actions specified in the spec. The next section talks about initialization.

What's going on in your first example is equivalently

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

( Once initialized, the sync block becomes useless; JVM will optimize it off. )

Semantically, this is not different from the 2nd impl. This doesn't really outshine "double checked locking", because it is double checked locking.

Since it piggybacks on class init semantics, it only works for static instances. In general, lazy evaluation is not limited to static instances; imagine there's an instance per session.

月下凄凉 2024-11-26 00:04:05

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

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

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

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

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

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

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

  • 实际上将被使用。

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

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

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

First off, does Java actually have this problem? I know order within a class is fully specified but does it somehow guarantee consistent order between classes (such as with a class loader)?

It does, but to a lesser degree than in C++:

  • If there is no dependency cycle, the static initialization occurs in the right order.

  • If there is a dependency cycle in the static initialization of a group of classes, then the order of initialization of the classes is indeterminate.

  • However, Java guarantees that default initialization of static fields (to null / zero / false) happens before any code gets to see the values of the fields. So a class can (in theory) be written to do the right thing irrespective of the initialization order.

Secondly, if the order is consistent, why would the lazy construction option ever be advantageous?

Lazy initialization is useful in a number of situations:

  • When the initialization has side effects that you don't want to happen unless the object is actually going to be used.

  • When the initialization is expensive, and you don't want it to waste time doing it unnecessarily ... or you want more important things to happen sooner (e.g. displaying the UI).

  • When the initialization depends on some state that is not available at static initialization time. (Though you need to be careful with this, because the state might not be available when lazy initialization gets triggered either.)

You can also implement lazy initialization using a synchronized getInstance() method. It is easier to understand, though it makes the getInstance() fractionally slower.

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

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

版本 1 是一个值得遵循的极好模式。

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

The code in the first version is the correct and best way to safely lazily construct a singleton.
The Java Memory Model guarantees that INSTANCE will:

  • Only be initialized when first actually used (ie lazy), because classes are loaded only when first used
  • Be constructed exactly once so it's completely thread-safe, because all static initialization is guaranteed to be completed before the class is available for use

Version 1 is an excellent pattern to follow.

EDITED
Version 2 is thread safe, but a little bit expensive and more importantly, severely limits concurrency/throughput

贪了杯 2024-11-26 00:04:05

我不喜欢你的代码片段,但我有你的问题的答案。是的,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)中,子数组的成员为空。该数组具有正确的大小,但其元素为空,因为在实例化数组时它们不可用。

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

I'm not into your code snippet, but I have an answer for your question. Yes, Java has an initialization order fiasco. I came across it with mutually dependent enums. An example would look like:

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; }
}

The key is that B.B1 must exist when creating instance A.A1. And to create A.A1 B.B1 must exist.

My real-life use case was bit more complicated - the relationship between the enums was in fact parent-child so one enum was returning reference to its parent, but the second array of its children. The children were private static fields of the enum. The interesting thing is that while developing on Windows everything was working fine, but in production—which is Solaris—the members of the child array were null. The array had the proper size but its elements were null because they were not available when the array was instantiated.

So I ended up with the synchronized initialization on the first call. :-)

乖乖哒 2024-11-26 00:04:05

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

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

使用如下:

Singleton reference1ToSingleton=Singleton.INST;    

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

The only correct singletone in Java can be declared not by class, but by enum:

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

The use is as:

Singleton reference1ToSingleton=Singleton.INST;    

All other ways do not exclude repeated instantiation through reflection or if the source of class is directly present in the app source. Enum excludes everything. ( The final clone method in Enum ensures that enum constants can never be cloned )

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文