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

And*_*niy 13 java concurrency multithreading final jls

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

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

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

在JLS之后,接下来的示例演示了如何不保证非最终字段的初始化(1Example 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 = f.x; // guaranteed to see 3 
           int j = f.y; // could see 0 
       } 
    } 
}
Run Code Online (Sandbox Code Playgroud)

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

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


所以,问题是:

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

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

是不是有些矛盾?


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

class FinalFieldExample { 
    final int x; 
    final int y; 
    ...
Run Code Online (Sandbox Code Playgroud)

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

if (f != null) { 
int i = f.x; // guaranteed to see 3
int j = f.y; // guaranteed to see 4, isn't it???
Run Code Online (Sandbox Code Playgroud)

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

Gra*_*ray 7

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

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

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

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

你不应该在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();
      }
  }
Run Code Online (Sandbox Code Playgroud)

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

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

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

static void writer() {
    f = new FinalFieldExample();
}
Run Code Online (Sandbox Code Playgroud)

这是至关重要的区别.