Java 内存模型:创建最终实例字段的循环引用图是否安全,所有这些都在同一个线程中分配?

Java Memory Model: Is it safe to create a cyclical reference graph of final instance fields, all assigned within the same thread?

比我更了解 Java 内存模型的人可以证实我对以下代码正确同步的理解吗?

class Foo {
    private final Bar bar;

    Foo() {
        this.bar = new Bar(this);
    }
}

class Bar {
    private final Foo foo;

    Bar(Foo foo) {
        this.foo = foo;
    }
}

我知道这段代码是正确的,但我还没有完成整个 happens-before 数学运算。我确实找到了两个非正式的引用,表明这是合法的,尽管我对完全依赖它们有点谨慎:

The usage model for final fields is a simple one: Set the final fields for an object in that object's constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object's constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object's final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are. [The Java® Language Specification: Java SE 7 Edition, section 17.5]

另一个参考:

What does it mean for an object to be properly constructed? It simply means that no reference to the object being constructed is allowed to "escape" during construction. (See Safe Construction Techniques for examples.) In other words, do not place a reference to the object being constructed anywhere where another thread might be able to see it; do not assign it to a static field, do not register it as a listener with any other object, and so on. These tasks should be done after the constructor completes, not in the constructor. [JSR 133 (Java Memory Model) FAQ, "How do final fields work under the new JMM?"]

不可变对象(只有 final 字段)只有 "threadsafe" 在正确构造后才存在,这意味着它们的构造函数已经完成。 (VM 可能通过此类对象的构造函数之后的内存屏障来实现此目的)

让我们看看如何使您的示例确实不安全:

  • 如果 Bar-Constructor 将 this-reference 存储在另一个线程可以看到它的地方,这将是不安全的,因为 Bar 尚未构造。
  • 如果 Bar-Constructor 将 foo 引用存储在另一个线程可以看到它的地方,这将是不安全的,因为 foo 尚未构造。
  • 如果 Bar-Constructor 会读取一些 foo-fields,那么(取决于 Foo-constructor 内部的初始化顺序)这些字段将始终未初始化。那不是线程安全问题,只是初始化顺序的影响。 (在构造函数中调用虚方法也有同样的问题)

对由 new 表达式创建的不可变对象(仅 final 字段)的引用始终可以安全访问(没有未初始化的字段可见)。但是,如果这些引用是由放弃其 this-reference 的构造函数获得的,则这些 final 字段中引用的对象可能会显示未初始化的值。

正如 Assylias 已经写的那样:因为在您的示例中,构造函数没有存储对另一个线程可以看到它们的位置的引用,所以您的示例是 "threadsafe"。创建的 Foo-Object 可以安全地分配给其他线程。

是的,很安全。您的代码不会引入数据竞争。因此,它已正确同步。 类 的所有对象在完全初始化状态下对访问这些对象的任何线程都是可见的。

对于你的例子来说,这相当 straight-forward to derive formally:

  1. 对于正在构造线程的线程,所有观察到的字段值需要与程序顺序保持一致。对于这个 线程内一致性 ,在构造 Bar 时,递交的 Foo 值被正确地观察到,而不是 null。 (这可能看起来微不足道,但内存模型也调节 "single threaded" 内存排序。)

  2. 对于获取 Foo 实例的任何线程,其引用的 Bar 值只能通过 final 字段读取。这在读取 Foo 对象的地址和取消引用指向 Bar 实例的对象字段之间引入了 取消引用排序

  3. 如果另一个线程因此能够完全观察Foo实例(正式来说,存在一个内存链),这个线程保证观察到这个 Foo 完全构造,这意味着它的 Bar 字段包含一个完全初始化的值。

请注意,如果实例只能通过 Foo 读取,那么 Bar 实例的字段本身就是 final 甚至都没有关系。添加修饰符并没有什么坏处,而且可以更好地记录意图,因此您应该添加它。但是,就内存模型而言,即使没有它你也会没事的。

请注意,您引用的 JSR-133 说明书仅描述了内存模型的实现,而不是内存模型本身。在很多方面,它过于严格。有一天,OpenJDK 可能不再与此实现保持一致,而是实现一个不那么严格但仍能满足正式要求的模型。 永远不要针对实现进行编码,始终针对规范进行编码!例如,不要依赖于构造函数之后放置的内存屏障,HotSpot 或多或少就是这样实现它的。这些东西不能保证保留,甚至可能因不同的硬件架构而有所不同。

你不应该让 this 从构造函数中引用 escape 的引用规则也太狭隘了。你不应该让它逃逸到另一个线程。例如,如果您将它交给一个虚拟分派的方法,您将无法再控制实例的最终位置。因此,这是一个非常糟糕的做法!但是,实际上不会分派构造函数,您可以按照您描述的方式安全地创建循环引用。 (我假设您可以控制 Bar 及其未来的变化。在共享代码库中,您应该严格记录 Bar 的构造函数不得让引用溜走。)