【问题标题】:About reference to object before object's constructor is finished关于在对象的构造函数完成之前对对象的引用
【发布时间】:2013-02-01 19:38:28
【问题描述】:

大家都知道JMM的这个特性,有时对对象的引用可以在这个对象的构造函数完成之前接收到值。

JLS7, p. 17.5 final Field Semantics我们也可以读到:

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

紧接着在 JLS 中的示例如下,它演示了如何不保证初始化 non-final 字段 (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 
       } 
    } 
}

另外,格雷先生在this问答中写道:

如果您将该字段标记为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 的所有字段都是final 时,为什么我们应该避免在对象f 的构造函数完成之前(根据(1))写入对它的引用?

【问题讨论】:

标签: java multithreading concurrency final jls


【解决方案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();
}

这是关键的区别。

【讨论】:

  • 好吧,你写道:例如,你不应该把一个对象放到一个被其他线程使用的静态字段中。看看来自 JLS7 的Example 17.5-1.。他们正是这样做的:static FinalFieldExample f; ... f = new FinalFieldExample(); 并保证 final f.x 将被正确初始化。
  • 所以他们从 within 构造函数@Andremoniy 中设置了静态?这就是问题所在。
  • 他们以某种静态方法执行此操作:static void writer() { f = new FinalFieldExample(); }
  • 对。这与在 within FinalFieldExample 构造函数中设置静态字段不同。这就是我所说的@Andremoniy。
  • 对不起,我还是不明白。您写道: 即使您的所有字段在一个类中都是 final 的,在构造函数完成之前将对该对象的引用共享给另一个线程也不能保证在其他线程开始使用该对象时已经设置了 final 字段。但在示例 17.5-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
        } 
    } 
}

如你所见,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 时。

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

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

【讨论】:

  • 您的第一行是否缺少 not Peter?
  • 还是不明白。我刚刚编辑了我的问题。在我的 2) 论文中,我的意思是,根据 (2) 和 (3) 我们可以安全地共享参考 before 构造函数完成。它直接来自示例(2)。不是吗?
  • @Andremoniy 我不明白你是如何得出这个结论的。它指出 final 在构造结束时提供了一些保证,而 non-final 则没有。不建议您在施工结束前有任何保证。
  • @Andremoniy 我已更改示例以说明这可能是一个问题。
  • 嗯,现在我明白了。非常感谢。
【解决方案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] 之后重新排序。

【讨论】:

  • 有道理。谢谢你的解释,+1!
【解决方案4】:

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

1) 根据陈述 (1) 我们应该避免共享参考 构造函数完成之前的不可变对象

阅读

1) 根据陈述 (1) 我们应该避免共享参考 可变对象在其构造函数完成之前

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


换句话说,你应该区分:

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

第二个是问题点。

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

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

【讨论】:

  • 不,在声明 (1) 中准确地说是关于 final 字段集。
  • @Andremoniy 在您的问题 1) 中,您的意思是说可变的还是不可变的?因为声明 1) 归结为您需要谨慎共享 mutable 对象。
  • 如果对象中的所有字段都是final,我们可以认为这个对象是immutable。这是 stmt (1) 谈到的一个案例。你能反驳吗?
  • @Andremoniy 不,我 100% 同意这种说法。我不同意的是您的问题部分内容为“1)根据声明(1),我们应该避免共享对不可变对象的引用......”。那应该是“...reference to mutable object...”
  • 好吧,在声明 (1) 中是在谈论带有 final 字段的对象。不是吗?还有人说,我们不应该在构造函数完成之前共享这个引用。不是吗?
猜你喜欢
  • 1970-01-01
  • 2015-07-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-30
  • 2011-12-29
  • 2019-03-19
  • 2023-03-31
  • 1970-01-01
相关资源
最近更新 更多