Lui*_*las 13 java concurrency multithreading memory-model java-memory-model
能比我更了解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;
}
}
Run Code Online (Sandbox Code Playgroud)
我知道这段代码是正确的,但我没有完成整个过程- 在数学之前.我确实找到了两个非正式的引文,表明这是合法的,尽管我有点担心完全依赖它们:
最终字段的使用模型很简单:在该对象的构造函数中设置对象的最终字段; 并且在对象的构造函数完成之前,不要在另一个线程可以看到的地方写入对正在构造的对象的引用.如果遵循此原因,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本.它还将看到那些最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样是最新的.[ Java®语言规范:Java SE 7 Edition,第17.5节 ]
另一个参考:
对象的正确构造意味着什么?它只是意味着在构造过程中不允许对正在构造的对象的引用"逃逸".(有关示例,请参阅安全构造技术.)换句话说,不要在另一个线程可能看到它的任何地方放置对正在构造的对象的引用; 不要将它分配给静态字段,不要将其注册为任何其他对象的侦听器,依此类推.这些任务应在构造函数完成后完成,而不是在构造函数中完成.[ JSR 133(Java内存模型)常见问题解答,"最终字段如何在新JMM下工作?" ]
是的,这是安全的.您的代码不会引入数据竞争.因此,它正确同步.这两个类的所有对象在其完全初始化状态下始终可见到访问对象的任何线程.
对于您的示例,这非常简单地正式派生:
对于构造线程的线程,所有观察到的字段值都需要与程序顺序一致.对于这种内部线程一致性,在构造时Bar,Foo正确地观察到手动值并且从不null.(这看起来似乎微不足道,但内存模型也规定了"单线程"内存排序.)
对于任何获取Foo实例的线程,其引用Bar值只能通过final字段读取.这引入了读取对象地址和指向实例的对象字段的解除引用之间的解除引用顺序.FooBar
如果另一个线程因此能够Foo完全观察实例(在形式上,存在一个存储器链),则保证该线程Foo完全构造这个,这意味着它的Bar字段包含一个完全初始化的值.
请注意,如果实例只能通过实例读取,则Bar实例的字段本身并不重要.添加修饰符不会伤害并更好地记录意图,因此您应该添加它.但是,在内存模型方面,即使没有它你也会没事的.finalFoo
请注意,您引用的JSR-133烹饪书仅描述了内存模型的实现,而不是内存模型本身.在许多方面,它太严格了.有一天,OpenJDK可能不再与这个实现保持一致,而是实现一个仍然满足形式要求的不太严格的模型.永远不要对实现进行编码,总是针对规范进行编码!例如,不要依赖于构造函数之后放置的内存屏障,这就是HotSpot或多或少地实现它的方式.这些东西不能保证保留,甚至可能因不同的硬件架构而有所不同.
引用的规则,你永远不应该让一个this引用从构造函数中转义,这个问题的视图也太狭隘了.你不应该让它逃到另一个线程.例如,如果您将其交给虚拟调度方法,则无法再控制实例最终的位置.因此这是一个非常糟糕的做法!但是,构造函数不会被虚拟调度,您可以按照您描述的方式安全地创建循环引用.(我假设您掌控着Bar它及其未来的变化.在共享代码库中,您应该严格记录构造函数Bar不能让引用滑出.)
不可变对象(只有最终字段)在正确构造之后才是"线程安全的",这意味着它们的构造函数已经完成.(VM可能在这些对象的构造函数之后通过内存屏障实现此目的)
让我们看看如何使你的例子肯定不安全:
对new-expression创建的不可变对象(仅最终字段)的引用始终可以安全访问(没有未初始化的字段可见).但是这些最终字段中引用的对象可能会显示未初始化的值,如果这些引用是由构造函数提供它的this-reference获得的.
正如Assylias已经写过:因为在你的例子中,构造函数没有存储对另一个线程可以看到它们的引用,你的例子是"线程安全".创建的Foo-Object可以安全地给予其他线程.
| 归档时间: |
|
| 查看次数: |
290 次 |
| 最近记录: |