关于在对象的构造函数完成之前对对象的引用

你们每个人都知道JMM的这个特性,有时候对象的引用可以这个对象的构造函数完成之前获得值。

在JLS7中,p。 17.5 最后的字段语义我们也可以阅读:

final字段的用法模型很简单:在该对象的构造函数中设置对象的final字段; 并且在对象的构造函数完成之前,不要在另一个线程可以看到的地方写入对正在构造的对象的引用 。 如果遵循此操作,那么当另一个线程看到该对象时,该线程将始终看到该对象的final字段的正确构造版本。 (1)

在JLS之后,接下来的例子展示了如何不保证非最终字段的初始化(1例17.5-1.1) (2)

 class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = fx; // guaranteed to see 3 int j = fy; // could see 0 } } } 

此外,在这个问答中,格雷先生写道:

如果将该字段标记为final则构造函数保证完成初始化作为构造函数的一部分。 否则,在使用锁之前,您必须同步锁定。 (3)


所以,问题是:

1)根据语句(1),我们应该避免在构造函数完成之前共享对不可变对象的引用

2)根据JLS给出的例子(2)和结论(3),似乎我们可以安全地构造函数完成之前共享对不可变对象的引用,即当它的所有字段都是final

是不是有些矛盾?


编辑-1 :我的意思是什么。 如果我们将以示例的方式修改类,那个字段y也将是final (2):

 class FinalFieldExample { final int x; final int y; ... 

因此,在reader()方法中,它将得到保证:

 if (f != null) { int i = fx; // guaranteed to see 3 int j = fy; // guaranteed to see 4, isn't it??? 

如果是这样,为什么我们应该避免在构造函数完成之前写入对象f引用(根据(1)),当f所有字段都是最终的?

[在JLS中围绕构造函数和对象发布]是不是存在一些矛盾?

我认为这些是略微不同的问题,并不矛盾。

JLS引用正在将对象引用存储在其他线程可以在构造函数完成之前可以看到它的位置。 例如,在构造函数中,您不应该将对象放入其他线程使用的static字段中,也不应该分叉线程。

  public class FinalFieldExample { public FinalFieldExample() { ... // very bad idea because the constructor may not have finished FinalFieldExample.f = this; ... } } 

你不应该在construtor中启动线程:

  // obviously we should implement Runnable here public class MyThread extends Thread { public MyThread() { ... // very bad idea because the constructor may not have finished this.start(); } } 

即使所有字段都是类的final字段,在构造函数完成之前将对象的引用共享给另一个线程也不能保证在其他线程开始使用该对象时已经设置了字段。

我的回答是在构造函数完成后讨论使用没有同步的对象。 这是一个稍微不同的问题,尽管类似于构造函数,缺乏同步以及编译器对操作的重新排序。

在JLS 17.5-1中,它们不在构造函数内部分配静态字段。 他们在另一个静态方法中分配静态字段:

 static void writer() { f = new FinalFieldExample(); } 

这是至关重要的区别。

在完整的例子中

 class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = fx; // guaranteed to see 3 int j = fy; // could see 0 } } } 

如您所见,直到构造函数返回才会设置f 。 这意味着fx是安全的,因为它是final并且构造函数已经返回。

在以下示例中,既不设置值也不保证值。

 class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; f = this; // assign before finished. } static void writer() { new FinalFieldExample(); } static void reader() { if (f != null) { int i = fx; // not guaranteed to see 3 int j = fy; // could see 0 } } } 

根据声明(1),我们应该避免在构造函数完成之前共享对不可变对象的引用

在构造因为多种原因(不可变或其他原因)之前,不应允许引用对象转义,例如,在存储对象后,对象可能会抛出exception。

根据JLS给出的示例(2)和结论(3),似乎我们可以安全地共享对不可变对象的引用,即当它的所有字段都是final时。

在构造对象之后,您可以安全地共享对线程之间的不可变对象的引用。

注意:在构造函数调用的方法中设置之前,您可以看到不可变字段的值。

建造出口在这里发挥着重要作用; JLS说“当c 退出时,对o的最终场f进行冻结动作”。 在构造函数退出之前/之后发布引用是非常不同的。

非正式地

 1 constructor enter{ 2 assign final field 3 publish this 4 }constructor exit 5 publish the newly constructed object 

[2]不能在构造函数出口之外重新排序。 所以[2]在[5]之后不能重新排序。

但[2]可以在[3]之后重新排序。

声明1)没有说出你的想法。 如果有的话,我会改写你的陈述:

1)根据语句(1),我们应该避免在构造函数完成之前共享对不可变对象的引用

阅读

1)根据声明(1),我们应该避免在构造函数完成之前共享对可变对象的引用

我所说的mutable是指具有任何非最终字段或对可变对象的最终引用的对象。 (不得不承认我不是100%你需要担心最终引用可变对象,但我认为我是对的…)


换句话说,你应该区分:

  • 最终字段(可能不可变对象的不可变部分)
  • 非最终字段,必须在任何人与此对象进行交互之前进行初始化
  • 在任何人与此对象交互之前不必初始化的非final字段

第二个是问题点。

因此,您可以共享对不可变对象的引用(所有字段都是final ),但是您需要谨慎使用具有非final字段的对象,这些字段必须在任何人都可以使用对象之前进行初始化。

换句话说,对于已编辑的JLS示例,您发布了两个字段都是finalint j = fy; 保证是最终的。 但这意味着你不需要避免写入对象f的引用,因为在任何人都可以看到之前它总是处于正确的初始化状态。 你不需要担心它,JVM会这样做。