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

2022-09-01 11:01:29

Goetz的Java Concurrency in Practice,第41页,提到了引用如何在构建过程中逃脱。一个“不要这样做”的例子:this

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

这里是通过引用封闭实例的事实来“转义”的。这种情况可以通过使用静态工厂方法(首先构造普通对象,然后注册侦听器)而不是公共构造函数(完成所有工作)来解决。这本书继续说:thisdoSomething(e)ThisEscape

从其构造函数中发布对象可以发布未完全构造的对象。即使发布是构造函数中的最后一个语句,也是如此。如果引用在构造过程中转义,则认为对象未正确构造。this

我不太明白这个。如果发布是构造函数中的最后一个语句,那么在此之前是否已经完成了所有构造工作?为什么到那时无效?显然,在那之后还有一些巫毒教,但那又如何呢?this


答案 1

构造函数的结尾在并发性方面是一个特殊的地方,相对于最终字段而言。来自 Java 语言规范的第 17.5 节

当对象的构造函数完成时,将被视为已完全初始化。如果线程只能在对象完全初始化后才能看到对该对象的引用,则保证可以看到该对象的最终字段的正确初始化值。

最终字段的使用模型很简单。在某个对象的构造函数中设置该对象的最终字段。不要在对象的构造函数完成之前,在另一个线程可以看到它的位置写入对对象的引用。如果遵循此命令,则当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到由最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样最新。

换句话说,如果侦听器在另一个线程中检查对象,则最终可能会看到具有默认值的最终字段。如果在构造函数完成后进行侦听器注册,则不会发生这种情况。

就正在发生的事情而言,我怀疑在构造函数的最末尾有一个隐式内存屏障,确保所有线程都“看到”新数据;如果没有应用该内存屏障,则可能存在问题。


答案 2

另一个问题出现在 Subclass ThisEscape 上,子类调用此概念器时。EventListener 中的 implicit this 引用将具有一个构造不完整的对象。