在链接构造函数时,JVM的隐式内存屏障如何表现?

时间:2010-03-25 08:00:56

标签: java constructor jvm compiler-optimization memory-barriers

参考我的earlier question on incompletely constructed objects,我有第二个问题。正如Jon Skeet指出的那样,在构造函数的末尾有一个隐式的内存障碍,可以确保所有线程都可以看到final个字段。但是如果构造函数调用另一个构造函数呢?在每个人的最后是否有这样的记忆障碍,或者只是在第一个被召唤的人的最后?也就是说,当“错误的”解决方案是:

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

正确的一个是工厂方法版本:

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        }
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}

以下是否也有效?

public class MyListener {
    private final EventListener listener;

    private MyListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        }
    }

    public MyListener(EventSource source) {
        this();
        source.register(listener);
    }
}

更新:基本问题是this()保证实际上调用上面的私有构造函数(在这种情况下会有预期的屏障)一切都是安全的,或私有构造函数可能将内联作为优化来保存一个内存屏障(在这种情况下,最终不会有障碍)公共构造函数)?

this()的规则是否恰好在某处定义?如果没有,那么我认为我们必须假设允许内联链接构造函数,并且可能是一些JVM甚至javac正在执行它。

5 个答案:

答案 0 :(得分:6)

我认为这是安全的,因为java内存模型声明:

  

o 成为一个对象,而 c o 的构造函数,其中一个是最终的   字段 f 已写入。对 o 的最终字段 f 进行冻结操作   当 c 正常或突然退出时。请注意,如果一个   构造函数调用另一个构造函数和调用的构造函数   设置一个最终字段,最终字段的冻结发生在   被调用的构造函数的结尾。

答案 1 :(得分:3)

  

当构造函数完成时,对象被认为是完全初始化的。

这也适用于链式构造函数。

如果必须在构造函数中注册,请将侦听器定义为静态内部类。这很安全。

答案 2 :(得分:3)

你的第二个版本不正确,因为它允许'this'引用从构造过程中逃脱。具有'this'转义会使初始化安全保证无效,从而使最终字段具有安全性。

为了解决隐含的问题,构造结束时的障碍只发生在对象构造的最后。一位读者提供的关于内联的直觉是有用的;从Java Memory Model的角度来看,方法边界不存在。

答案 3 :(得分:1)

编辑在建议编译器内联私有构造函数的评论之后(我没有想到这个优化)可能是代码不安全。而不安全的多线程代码最糟糕的部分似乎是有效的,所以你最好完全避免它。如果你想玩不同的技巧(你确实想要出于某些原因避免工厂),可以考虑添加一个包装器来保证内部实现对象中数据的一致性并在外部对象中注册。


我的猜测是它会很脆弱但是还可以。编译器无法知道内部构造函数是否只能从其他构造函数中调用,因此必须确保结果对于仅调用内部构造函数的代码是正确的,因此它使用的任何机制(内存屏障?)都有在那里。

我猜测编译器会在每个构造函数的末尾添加内存屏障。问题仍然存在:你正在将this引用传递给其他代码(可能是其他线程),然后才能完全构造 - 这很糟糕 - 但是如果剩下的唯一“构造”就是注册监听器,然后对象状态和以前一样稳定。

解决方案脆弱,因为在某些其他日子,您或其他程序员可能需要向对象添加另一个成员,并且可能会忘记链式构造函数是并发技巧并且可能决定初始化公共构造函数中的字段,这样做会增加难以检测应用程序中潜在的数据竞争,所以我会尽量避免使用该构造。

BTW:猜测的安全性可能是错误的。我不知道编译器是多么复杂/聪明,以及内存屏障(或类似)是否可​​以尝试优化...因为构造函数是私有的,编译器确实有足够的信息来知道它是只从其他构造函数调用,这足以确定内部构造函数中不需要同步机制...

答案 4 :(得分:1)

在c-tor中转义对象引用可以发布未完全构造的对象。如果发布是构造函数中的最后一个语句,即使也是如此。

您的SafeListener在并发环境中可能无法正常运行,即使执行了c-tor内联(我认为它不是 - 考虑通过访问私有c-tor使用反射创建对象)。