Java 内存模型:创建最终实例字段的循环引用图(所有字段均在同一线程内分配)是否安全?

2024-01-17

比我更了解 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 字段的使用模型很简单:在对象的构造函数中设置该对象的 Final 字段;并且不要将对正在构造的对象的引用写入到另一个线程可以在对象的构造函数完成之前看到它的地方。如果遵循这一点,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到这些最终字段引用的任何对象或数组的版本,这些版本至少与最终字段一样最新。 [Java® 语言规范:Java SE 7 版本,第 17.5 节 https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5]

另一个参考:

正确构造一个对象意味着什么?它只是意味着在构造过程中不允许“逃逸”对正在构造的对象的引用。 (有关示例,请参阅安全构造技术。)换句话说,不要将对正在构造的对象的引用放置在其他线程可能能够看到它的任何地方;不要将其分配给静态字段,不要将其注册为任何其他对象的侦听器,等等。这些任务应该在构造函数完成之后完成,而不是在构造函数中完成。 [JSR 133(Java 内存模型)常见问题解答, “在新的 JMM 下,final 字段如何工作?” http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalRight]


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

对于你的例子来说,这相当形式上直接推导 http://www.slideshare.net/VladimirSitnikv/final-field-semantics:

  1. 对于正在构造线程的线程,所有观察到的字段值需要与节目顺序。为了这线程内一致性,构建时Bar, 双手Foo值被正确观察并且永远不会null。 (这可能看起来微不足道,但内存模型也调节“单线程”内存顺序。)

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

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

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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 内存模型:创建最终实例字段的循环引用图(所有字段均在同一线程内分配)是否安全? 的相关文章

随机推荐