static class A {
    private static class InstanceHolder {
        private static final A a = new A();
    }

    private final B b;

    private A() {
        b = B.getInstance();
    }

    static A getInstance() {
        return InstanceHolder.a;
    }
}

static class B {
    private static class InstanceHolder {
        private static final B b = new B();
    }

    private final A a;

    private B() {
        a = A.getInstance();
    }

    static B getInstance() {
        return InstanceHolder.b;
    }
}

public static void main(String[] args) {
    A a = A.getInstance();
    B b = B.getInstance();
    System.out.println(a.b);
    System.out.println(b.a);
}

我预计即使是对构造函数A -> B -> A的间接调用也会导致StackOverflowError.

相反,第一个对象(a)初始化了对b的引用,而第二个对象的反向引用为空.

推荐答案

不是的.JVM跟踪"类初始化已开始",并且永远不会再次启动它.这是why整个InstanceHolder首歌曲和舞蹈 routine 的首要工作:您使用的是JVM的并发门控机制,它应用于类加载,否则其中的none将是完全有用的.

因此,您是absolutely guaranteedA.InstanceHolder的静态初始化式序列中的new A()最多运行一次ever.

这就提出了一个显而易见的问题:好吧,那么,当A.InstanceHolder的静态初始化过程已经开始,但还没有完成时,如果一个上下文访问A.InstanceHolder类,会发生什么?假设JVM保证不会重新运行初始化器,那么答案不可能是‘stackoverflow’,但是does会发生什么呢?

这可以分为两种不同的情况:

  1. 需要A.InstanceHolder才能初始化的代码正在不同的线程中运行.也就是说,another thread处于初始化过程中间(仍在经历实现new A()所需字节码序列,这是A.InstanceHolder的静态初始化码).在这种情况下,该线程将简单地阻塞.它几乎在各个方面都像synchronized.

  2. 需要A.InstanceHolder才能初始化的代码已在此线程中运行.换句话说,我们需要A.InstanceHolder来初始化in the middle of字节码序列.正在进行我们现在所依赖的初始化!在本例中,JVM规范是明确的:在初始化时,所有字段都被初始化为null/0/false,然后进行初始化.在这些字段可用之前,任何从它们读取的try 都只返回它们拥有的值(null/0/false),even if they are final.这确实导致了有点奇怪的情况,private static final String x = someMethodThatMakesX();仍然可以让x持有两个不同的值(null和实际值),即使它是最终的.我们可以简单地见证这一点:

> cat ExampleOfClassInitWeirdness.java
public class ExampleOfClassInitWeirdness {
  private static final String y = hi();
  private static final String x = test();
  private static final String z = bye();

  public static String hi() {
    System.out.println(x); // compiles. Prints 'null'
    return null;
  }

  public static String test() {
    System.out.println(x); // compiles. Prints 'null'
    return "hello";
  }

  public static String bye() {
    System.out.println(x); // compiles. Prints '"hello"'.
    return null;
  }

  public static void main(String[] args) {}
}
> java ExampleOfClassInitWeirdness.java
null
null
hello

这三个打印来自不同的地方,存在完全相同的Java代码:System.out.println(x);,假设xfinal,它如何打印不同的值呢?

是的,它可以--JVM规范是明确的,即使结果有点奇怪.

同样的原理解释了为什么不会出现堆栈溢出错误.顺序如下.我假设您在Java代码中的某个地方写了:A.getInstance().然后:

  • ClassLoader判断是否加载了A,以及其初始化状态是什么.
  • ClassLoader得出结论认为它没有加载,因此init状态为NEEDED.
  • ClassLoader从JAR文件或其他文件中找到字节码,并使用它创建类.
  • ClassLoader现在开始初始化过程.它自动执行此操作:判断状态是否仍为NEEDED.因此,它将更新为INITIALIZING,并跟踪该线程正在执行初始化的事实.初始化微不足道,甚至根本不存在;没有static{}个块,也没有初始化静态字段的表达式.我将跳过解释B是如何初始化的;它同样令人厌烦.S的状态变成了INITIALIZED岁.
  • 请记住,我们正在运行操作码INVOKESTATIC com.foo.pkg.A getInstance()Lcom/foo/pkg/A;,作为其中的一部分,我们必须确保在继续之前加载和初始化A.那么,让我们继续.A.getInstance()现在可以运行了.这运行操作码GETSTATIC com.foo.pkg.A$InstanceHolder a Lcom/foo/pkg/A;,因此,这需要加载和初始化A$InstanceHolder.不是的,所以...转到JAR文件,我们找到字节码并加载它,然后是初始化它的时间;它的状态为INITIALIZING,该线程作为初始化器.所有静态字段均初始化为null/0/false.
  • 有事情要做--运行代码new A().
  • 这需要运行代码B.getInstance()(这是另一个INVOKESTATIC操作码).
  • 这导致需要加载和初始化B.正如所讨论的,无聊,这种情况时有发生.
  • B.getInstance()英里.这导致GETSTATICB$InstanceHolder上,这意味着需要加载和初始化B$InstanceHolder.不是的.
  • 开始初始化过程.运行静态初始值设定项,这里是new B().
  • new B()除以A.getInstance()INVOKESTATIC
  • 这需要加载和初始化A个.它是.
  • 我们跑了A.getInstance()英里.这是来自A$InstanceHolderGETSTATICs,它需要加载和初始化A$InstanceHolder.它不是;不完全是-它是上了膛的,是的.但其状态为INITIALIZING,未初始化.我们在这里是堆栈的几层,但这都是由初始化A$InstanceHolder引起的事件链.
  • 注册为执行初始化的线程就是这个线程,所以我们不会阻塞,我们只是继续,不做任何修改.我们的行为就好像事情已经初始化了一样.所以,我们只是...执行GETSTATIC操作码.它尽职尽责地返回当前值.也就是null.
  • 现在已经完成了B.getInstance()个.此实例的a字段现在为null.
  • 现在已经完成了new B()个.这样,A$InstanceHolder的静态初始化序列现在就完成了.它生成的A实例有一个类型为B的字段;字段保存的B实例有一个保存A实例的字段.那个场地可容纳null人.
  • A.getInstance()结束并返回that.getB().getA()null的实例.

要理解的一件事是,finalmostly,javac是虚构的.JVM大多不关心最终结果,并将其视为一种注释.原因是:

public class Example {
  private static final Instant NOW = Instant.now();

  public static void breakThings() {
    NOW = null;
  }
}

不编译是因为javac拒绝编译它.在javac中的某个地方有一个if语句,它说:嗯,第NOW号是最终版本,您可能会try 多次写入它,所以,不是.

但只需从Java代码中删除If,它就会编译并运行.

与泛型不同,从JVM的Angular 来看,泛型实际上是entirely个注释(它不会影响JVM所做的事情,无论如何,关心泛型的是entirely个javac,这解释了为什么您不能在运行时从实例中读取它们),final只是mostly个注释.某些并发规则和保证以及某些热点优化的工作方式有所不同.

Java相关问答推荐

我应该避免在Android中创建类并在运行时编译它们吗?

具有额外列的Hibert多对多关系在添加关系时返回NonUniqueHealthExcellent

Java应用程序崩溃时试图读取联系人从电话

Java中后期绑定的替代概念

错误:在Liferay7.4中找不到符号导入com.liferay.portal.kernel.uuid.PortalUUID;";

JavaFX Maven Assembly插件一直打包到错误的JDK版本

';com.itextpdf.ext.html.WebColors已弃用

蒙蒂霍尔比赛结果不正确

Spark忽略Iceberg Nessie目录

Spring data JPA/Hibernate根据id获取一个列值

当Volatile关键字真的是必要的时候?

使用多个RemoteDatabase对象的一个线程

Java Telnet客户端重复的IAC符号

如何在Jooq中获取临时表列引用?

如何创建模块信息类文件并将其添加到JAR中?

找出承载Cargo 的最小成本

组合连接以从两个表返回数据

原始和参数化之间的差异调用orElseGet时可选(供应商)

窗口启动后不久,从java.awt.Graphics disapear创建的矩形

为什么当我输入变量而不是直接输入字符串时,我的方法不起作用?