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

比我更了解 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;
    }
}

我知道这段代码是正确的,但我还没有完成整个发生 - 在数学之前。我确实发现了两个非正式的引文,表明这是合法的,尽管我对完全依赖它们有点警惕:

最终字段的使用模型很简单:在对象的构造函数中设置对象的最终字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到它的位置写入对正在构造的对象的引用。如果遵循此命令,则当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到由最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样最新。[Java® 语言规范:Java SE 7 版,第 17.5 节]

另一个参考:

正确构造对象意味着什么?它只是意味着在构造过程中不允许对正在构造的对象的引用“逃脱”。(有关示例,请参阅安全施工技术。换句话说,不要将对正在构造的对象的引用放在另一个线程可能能够看到它的任何位置;不要将其分配给静态字段,不要将其注册为任何其他对象的侦听器,依此类推。这些任务应在构造函数完成后完成,而不是在构造函数中完成。[JSR 133(Java 内存模型)常见问题解答,“最终字段如何在新的 JMM 下工作?]


答案 1

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

对于您的示例,这非常直接地正式派生

  1. 对于构造线程的线程,所有观察到的字段值都需要与程序顺序一致。对于这种线程内一致性,在构造时,正确观察手值,从不观察 。(这可能看起来微不足道,但内存模型也调节“单线程”内存顺序。BarFoonull

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

  3. 因此,如果另一个线程能够完全观察实例(在正式术语中,存在一个内存链),则保证该线程能够完全构造,这意味着其字段包含完全初始化的值。FooFooBar

请注意,如果实例只能通过 读取,则实例的字段本身甚至无关紧要。添加修饰符不会造成伤害,并且可以更好地记录意图,因此您应该添加它。但是,内存模型方面,即使没有它,你也会没事的。BarfinalFoo

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

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


答案 2

不可变对象(仅包含最终字段)仅在正确构造后才是“线程安全”,这意味着它们的构造函数已完成。(VM 可能通过此类对象的构造函数之后的内存屏障来实现此目的)

让我们看看如何使您的示例肯定是不安全的:

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

对由 new 表达式创建的不可变对象(仅最终字段)的引用始终可以安全访问(看不到未初始化的字段)。但是,如果这些引用是由构造函数获取的,则这些最终字段中引用的对象可能会显示未初始化的值。

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


推荐