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

我知道这段代码是正确的,但我没有完成整个过程– 在数学之前 。 我确实找到两个非正式的引用,表明这是合法的,虽然我有点担心完全依赖它们:

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

另一个参考:

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

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

对于您的示例,这非常简单地正式派生 :

  1. 对于构造线程的线程,所有观察到的字段值都需要与程序顺序一致。 对于这种线程内一致性 ,在构造Bar ,正确观察到的Foo值并且永远不会为null 。 (这看起来似乎微不足道,但内存模型也规定了“单线程”内存排序。)

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

  3. 如果另一个线程因此能够完全观察Foo实例(在forms上,存在一个存储器链 ),则保证该线程完全构造这个Foo ,这意味着它的Bar字段包含完全初始化的值。

请注意,如果实例只能通过Foo读取,那么Bar实例的字段本身也是final的。 添加修饰符不会伤害并更好地记录意图,因此您应该添加它。 但是,在内存模型方面,即使没有它,你也会没事的。

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

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

不可变对象(只有最终字段)在正确构造后才是“线程安全”,这意味着它们的构造函数已经完成。 (VM可能在这些对象的构造函数之后通过内存屏障完成此操作)

让我们看看如何使你的例子肯定不安全:

  • 如果Bar-Constructor会在另一个线程可以看到它的情况下存储this-reference,那么这将是不安全的,因为Bar还没有构建。
  • 如果Bar-Constructor会存储一个foo-reference,而另一个线程可以看到它,这将是不安全的,因为foo还没有被构造。
  • 如果Bar-Constructor会读取一些foo字段,那么(取决于Foo构造函数中的初始化顺序)这些字段将始终未初始化。 这不是一个线程安全问题,只是初始化顺序的影响。 (在构造函数中调用虚方法具有相同的问题)

对new-expression创建的不可变对象(仅最终字段)的引用始终是安全的(没有未初始化的字段可见)。 但是这些最终字段中引用的对象可能会显示未初始化的值,如果这些引用是由构造函数获取它的this-reference获得的。

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