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

2022-09-04 20:54:42

你们每个人都知道JMM的这个功能,有时在这个对象的构造函数完成之前,对对象的引用可以接收值。

JLS7 第 17.5 页的最后一个字段语义中,我们也可以读到:

字段的使用模型很简单:在对象的构造函数中设置对象的字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到它的位置写入对正在构造的对象的引用。如果遵循此命令,则当另一个线程看到该对象时,该线程将始终看到该对象字段的正确构造版本。finalfinalfinal(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 = f.x; // guaranteed to see 3 
           int j = f.y; // could see 0 
       } 
    } 
}

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

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


所以,问题是:

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

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

是不是有些矛盾?


编辑1:我到底是什么意思。如果我们以这种方式修改类,则该字段也将是(2):yfinal

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

因此,在方法上可以保证:reader()

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

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


答案 1

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

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

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

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

你也不应该在构造器中启动线程:

  // 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();
}

这是关键的区别。


答案 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
        } 
    } 
}

如您所见,直到构造函数返回才设置。这意味着是安全的,因为它是并且构造函数已返回。ff.xfinal

在下面的示例中,不保证要设置任何值。

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 = f.x;  // not guaranteed to see 3  
            int j = f.y;  // could see 0
        } 
    } 
}

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

由于多种原因(不可变或其他明智的原因),在构造对象转义之前,您不应允许对对象转义的引用,例如,该对象可能会在您存储对象后引发异常。

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

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

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


推荐