13

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

JLS7 中,p。17.5 final 字段语义我们还可以阅读:

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

紧随其后的是 JLS 中的示例,该示例演示了如何不保证初始化 非最终(2)字段(1Example 17.5-1.1) :

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

是不是有些矛盾?


EDIT-1:我的意思是什么。如果我们以这种方式修改示例中的类,该字段y也将是final(2):

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???

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

4

4 回答 4

7

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

这是关键的区别。

于 2013-02-01T19:46:32.753 回答
4

在完整的例子中

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

如您所见,f直到构造函数返回后才设置。这意味着f.x是安全的,因为它是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 = f.x;  // not guaranteed to see 3  
            int j = f.y;  // could see 0
        } 
    } 
}

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

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

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

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

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

于 2013-02-01T19:52:25.657 回答
3

构造出口在这里起着重要作用;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] 之后重新排序。

于 2013-02-01T20:53:29.100 回答
1

陈述 1) 并没有说出你认为它做了什么。如果有的话,我会改写你的陈述:

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

读书

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

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


换句话说,您应该区分:

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

第二个是问题点。

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

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

于 2013-02-01T19:49:29.963 回答