Java:通过_happens-before_关系在构造函数中“泄漏”此引用是否安全?

2022-09-03 08:02:51

Goetz 的“Java 并发实践”的第 3.2.1 节包含以下规则:

在施工期间不允许引用逃逸this

我理解,一般来说,允许转义会导致其他线程看到对象的不完整构造版本,并违反字段的初始化安全保证(例如,在这里讨论)thisfinal)

但是,是否有可能安全地泄漏?特别是,如果您在泄漏之前建立了先发生关系?

例如,官方执行器Javadoc

在将对象提交到 happen 之前(在其执行开始之前,可能在另一个线程中)在线程中的操作RunnableExecutor

我对Java内存模型的天真理解是,像下面这样的东西应该是安全的,即使它在构造函数结束之前泄漏:this

public final class Foo {
  private final String str1;
  private String str2;
  public Foo(Executor ex) {
    str1 = "I'm final";
    str2 = "I'm not";
    ex.execute(new Runnable() {
      // Oops: Leakage!
      public void run() { System.out.println(str1 + str2);}
    });
  }
}

也就是说,即使我们已经泄漏到一个潜在的恶意,在泄漏之前和发生之前,对象是完全构造的,即使它还没有按照JLS 17.5“完全初始化”。thisExecutorstr1str2

请注意,我还要求类为 ,因为任何子类的字段都将在泄漏后初始化。final

我在这里错过了什么吗?这真的能保证表现良好吗?在我看来,这就像是“搭载同步”(16.1.4)的一个合法例子,总的来说,我非常希望能指出任何涉及这些问题的额外资源。

编辑:我知道,正如@jtahlborn所指出的,我可以通过使用公共静态工厂来避免这个问题。我正在寻找问题的答案,以巩固我对Java内存模型的理解。

编辑#2这个答案暗示了我试图得到的东西。也就是说,遵循其中引用的JLS规则足以保证所有字段的可见性。但这是必要的,还是我们可以利用其他先发生机制来确保我们自己的可见性保证?final


答案 1

你是对的。通常,Java 内存模型不以任何特殊方式处理构造函数。在构造函数退出之前或之后发布对象引用几乎没有什么区别。

当然,唯一的例外是关于领域。构造函数的退出,其中写入最终字段以定义字段上的“冻结”操作;如果在 之后发布,即使没有发生之前的边缘,其他线程也会读取正确初始化的字段;但如果 在 之前发布,则不会。finalthisfreezethisfreeze

有趣的是,如果有构造函数链接,则在最小范围内定义;例如:freeze

-- class Bar

final int x;

Bar(int x, int ignore)
{
    this.x = x;  // assign to final
}  // [f] freeze action on this.x

public Bar(int x)
{ 
    this(x, 0);
    // [f] is reached!
    leak(this); 
}

这是安全的 w.r.t. .leak(this)this.x

有关字段的更多详细信息,请参阅我的其他答案final


如果看起来太复杂,那就是。我的建议是 - 忘记它!永远不要依赖字段语义来不安全地发布。如果程序正确同步,则无需担心字段或其微妙的语义。不幸的是,目前的气候是尽可能地推动领域,给程序员带来了不适当的压力。finalfinalfinalfinal


答案 2