“不完整构造的对象”是什么?

31

在Goetz的《Java并发编程实践》第41页中提到,this引用可能在构造期间逃逸。以下是一个“不要这样做”的例子:

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            });
    }
}

这里通过doSomething(e)引用到封闭的ThisEscape实例,实现了"逃逸"。可以通过使用静态工厂方法(先构造普通对象,然后注册监听器)而非公共构造函数(执行所有工作)来解决这种情况。书中继续讲述:

在构造函数内部发布对象可能会发布一个未完全构造的对象。即使发布是构造函数中的最后一条语句也是如此。如果this引用在构造过程中泄漏出去,那么该对象被认为是未经适当构造的

我不太理解这个问题。如果发布是构造函数中的最后一条语句,那么所有的构造工作不都已经完成了吗?为什么此时this无效?显然,在此之后发生了某些神奇的事情,但是是什么呢?


相反,我不明白这个“this”引用将如何传递给EventListener,因为EventListener将首先被构建,然后才是ThisEscape - Anand Kadhi
3个回答

20
构造函数的结尾在并发方面是一个特殊的位置,涉及到最终字段。来自Java语言规范第17.5节的内容如下:
一个对象在其构造函数完成时被认为是完全初始化的。只能在对象完全初始化后才能看到对该对象的引用的线程保证能够看到该对象的最终字段的正确初始化值。
最终字段的使用模型是简单的。在对象的构造函数中设置该对象的最终字段。不要在另一个线程可以看到它之前,在正在构造的对象的引用处写入对该对象的引用。如果遵循这个原则,那么当对象被另一个线程看到时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到由这些最终字段引用的任何对象或数组的版本,至少与最终字段一样更新。
换句话说,如果在另一个线程中检查对象,则侦听器最终可能会看到带有默认值的最终字段。如果在构造函数完成后进行侦听器注册,则不会发生这种情况。
就发生了什么而言,我怀疑在构造函数的最后会有一个隐式的内存屏障,确保所有线程“看到”新数据;如果没有应用该内存屏障,则可能会出现问题。

@Joonas:这就是问题所在——如果你确保引用不会从构造函数中逃逸,它们就可以很好地支持并发。在大多数情况下,这是一个相当小的代价。 - Jon Skeet
1
实际上,这适用于任何领域,而不仅仅是最终领域。 - Stephen C
1
顺便问一下,是否制作另一个简单的构造函数来初始化所有字段,然后在泄露“this”的更复杂的构造函数的开头调用this(),这样就足够了吗?这是否类似于工厂方法? - Joonas Pulakka
@Stephen C:如果您查看规范链接,您会发现它仅特别针对最终字段进行了调用。 - Jon Skeet
一个线程只有在对象被完全初始化后才能看到对该对象的引用,这样可以保证该线程看到该对象的最终字段正确初始化的值。这句话很难理解。是什么让线程只能在对象完全初始化后才能看到对该对象的引用?是否有一些线程可以看到未完全初始化的对象?从措辞上看,似乎这个保证只适用于某些线程,而不是所有线程。 - tmsimont
显示剩余4条评论

6

当你对ThisEscape进行子类化并且子类调用此构造函数时,会出现另一个问题。EventListener中的隐式this引用将会是一个未完全构建的对象。


1
好的决定。特别是如果子类覆盖了ThisEscape中的虚拟方法,这可能会创建问题 - 在设置它们所需的状态之前,这些被覆盖的方法可能会被调用。 - Jon Skeet
1
@Stephen C:我认为这是一个非常好的观点,绝对不是离题的。 - Joonas Pulakka
@JoonasPulakka 这是一个很好的、离题的观点。问题是关于最终字段的。 - user207421
@EJP 这个问题涉及到不完整构造的对象,尽管被接受的答案(很遗憾只能接受一个!)讨论了final字段的某种奇特行为。 - Joonas Pulakka
为什么Java不能有一种方式来指示对象创建已完成并且可以安全地使用该对象。因此,在任何线程中引用不完整的对象将等待对象完全构造。 - ansraju

2
在 `registerListener` 结束和构造函数返回之间存在一个短暂但有限的时间段。在此期间,另一个线程可能会进来并尝试调用 `doSomething()`。如果运行时此时没有直接返回到您的代码,则该对象可能处于无效状态。
我不确定 Java 是否适用,但我可以想到一个可能的例子是,运行时在返回给您之前重新定位了实例。
我承认这只是一个很小的机会。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接