通过枚举方式的单例是惰性初始化的吗?

Posted

技术标签:

【中文标题】通过枚举方式的单例是惰性初始化的吗?【英文标题】:Singleton via enum way is lazy initialized? 【发布时间】:2013-05-22 04:53:30 【问题描述】:

这是一个非常普遍的枚举单例代码:

public enum enumClazz
   INSTANCE
   enumClazz()
     //do something
   

还有一堆地方说是惰性初始化。但是我读了'Inside the Java Virtual Machine'的第7章后感到困惑——类型的生命周期:

Java 虚拟机规范给出了实现 类和接口加载和链接时间的灵活性, 但严格定义了初始化的时间。所有实现 必须在第一次主动使用时初始化每个类或接口。这 以下六种情况符合主动使用条件:

创建了一个类的新实例(在字节码中,新指令的执行。或者,通过隐式创建, 反射、克隆或反序列化。) 类声明的静态方法的调用(在字节码中,invokestatic 指令的执行) 类或接口声明的静态字段的使用或赋值,但最终的静态字段除外并由 编译时常量表达式(在字节码中,执行 getstatic 或 putstatic 指令) Java API 中某些反射方法的调用,例如 Class 类或 java.lang.reflect 中的类中的方法 包 类的子类的初始化(类的初始化需要先对其超类进行初始化。) 在 Java 虚拟机启动时将一个类指定为初始类(使用 main()

第三点加粗说明如果字段为static final,则该字段的初始化发生在编译时。同样,enumClazz 中的INSTANCE 隐式等于public static final 并符合第三点。

如果我的理解有误,有人可以纠正我吗?

【问题讨论】:

【参考方案1】:

enum 实例字段不是“由编译时常量表达式初始化”。他们 不可能,因为only String and primitive types are possible types for a compile-time constant expression。

这意味着当INSTANCE第一次被访问时这个类会被初始化(这正是我们想要的效果)。

存在上述粗体文本中的异常,因为这些常量(使用编译时常量表达式初始化的static final 字段)将在编译期间有效地内联:

class A 
  public static final String FOO = "foo";

  static 
    System.out.println("initializing A");
  


class B 
  public static void main(String[] args) 
    System.out.println(A.FOO);
  

在此示例中执行类 B初始化 A(并且将打印“正在初始化 A”)。如果您查看为B 生成的字节码,您会看到一个字符串文字,其值为“foo”并且no 引用了A 类。

【讨论】:

这意味着在第一次访问INSTANCE 时会初始化该类。如果 enum 类有 static 方法(任何 - 使其成为 void 并且什么都不做),则当调用此 static void 方法时,此枚举类及其 all 实例将立即初始化。 (!)所以,你不需要访问INSTANCE 来初始化它——调用那个额外的方法。完毕。令人困惑的是,这种枚举方式是如何惰性的。它就像简单的带有 final 字段的热切单例实现一样懒惰 - 你调用它的 static void 方法并且 final instance 字段被初始化(第一次访问时懒惰?)。 @uvsmtid:我不确定你想说什么。是的,显然在枚举类上调用方法也会强制它被初始化,就像在类上调用任何方法都会确保该类被初始化一样。 正确。现在,如果它是另一个惰性实现(例如双重检查锁定),则在调用 getInstance() 之前不会初始化单例实例 - 我可以看到它是如何惰性的。我没有看到枚举方法比最简单的渴望单例实现懒惰:在这两种情况下,实例在第一次使用类的任何其他(静态)方法时被初始化。 没有人争辩说这在某种程度上比其他实现更“懒惰”。这只是实现该模式的另一种方式,其中 JVM 有助于解决令人讨厌的可靠地初始化下多线程访问但仅最多一次问题。就个人而言,我更喜欢使用通过一些 DI 框架实现的单例,而不是这种模式或双重检查模式,但这不在这个问题的范围内。这个问题是关于“如果值是编译时间常数,这怎么会懒惰”(答案是:它不是编译时间常数)。 嗯,标题ls “Singleton via enum way is lazy initialized?” 内容说“一堆地方都说是惰性初始化”。其他问答将枚举方式称为惰性。所以这一切确实表明它比渴望的东西更懒惰。 NVM,我只是发布了direct question。【参考方案2】:

加粗的第三点说明如果该字段是'static final',则该字段的初始化发生在编译时

不完全是 - 它仅适用于“最终的静态字段并由编译时常量表达式初始化”:

static final String = "abc"; //compile time constant
static final Object = new Object(); //initialised at runtime

在您的情况下,将在加载枚举类时初始化单例,即第一次在您的代码中引用 enumClazz

所以它实际上是懒惰的,除非你的代码中的其他地方有使用枚举的语句。

【讨论】:

class Singletonpublic static final instance = new Singleton();... 那么,对于泛型类的方式,这个“实例”是惰性初始化的? 是的,它会以同样的方式延迟初始化。 Class<?> c = EnumClazz.class; 这样的语句不会触发EnumClazz 的初始化。事实上,如果不实际使用它,很难触发它的初始化。值得强调的是,加载初始化是两个不同的东西。

以上是关于通过枚举方式的单例是惰性初始化的吗?的主要内容,如果未能解决你的问题,请参考以下文章

DCL的单例一定是线程安全的吗

看完MJ讲解的单例后的个人总结

几种单例模式实现方式及其优缺点分析

线程安全的单例模式

C# 中的单例是啥?

13.scala的单例对象