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

14

关于我之前的问题有关未完全构建的对象, 我有第二个问题。就像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个回答

6

根据Java内存模型的规定,我认为这是安全的:

o为一个对象,c为构造函数,其在其中写入了一个final字段f。当c正常或异常退出时,将对o的最终字段f进行冻结操作。请注意,如果一个构造函数调用另一个构造函数,并且被调用的构造函数设置了一个final字段,则最终字段的冻结将在被调用的构造函数结束时发生。


1
这是唯一一个有权威参考支持的答案。(这里是实际参考链接:http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html)。我想知道的唯一问题是,*freeze* 究竟意味着什么,没有任何地方解释过(或者至少我没有找到)。 - Joonas Pulakka
1
我曾经遇到过同样的问题,非正式的描述在(http://www.cs.umd.edu/~pugh/java/memoryModel/CommunityReview.pdf),更正式的定义可以在(http://www.cs.umd.edu/~pugh/java/memoryModel/newFinal.pdf)中找到。 - Giambattista Bloisi

3

当一个对象的构造函数结束时,它被认为是完全初始化的。

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

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


2
你确定编译器不会检测到私有构造函数被另一个构造函数调用,然后它会被内联到调用(公共)构造函数中,以节省一个内存屏障吗? - Joonas Pulakka

3

您的第二个版本是不正确的,因为它允许“this”引用从构造过程中逃逸出来。如果“this”逃逸,就会使得给final字段赋值时的初始化安全性保证失效。

针对隐含的问题,构造函数末尾处的屏障仅在对象构造的最后发生。一位读者提供的有关内联的直觉是有用的;从Java内存模型的角度来看,方法边界不存在。


我想你的意思是第三个版本(MyListener)不正确?第二个版本来自JCIP,据说可以防止this逃逸。但还是谢谢!因此,调用this()并不是构造函数调用的意义上,它不会使对象完全构造;只有在首先被调用的构造函数返回后,对象才被完全构造。 - Joonas Pulakka
2
稍等片刻。17.5.1(在非规范性讨论中)指出:“请注意,如果一个构造函数调用另一个构造函数,并且被调用的构造函数设置了一个final字段,则最终字段的冻结将在被调用的构造函数结束时发生。” 这似乎表明最后一个示例是安全的。 当然,实际实现完全是另一回事。 - Tom Hawtin - tackline
@Tom:哇!它确实是这样写的。这不是官方规范吗?那么实现应该按照这种方式进行,否则它们就会出问题。 - Joonas Pulakka

1

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


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

我猜测编译器会在每个构造函数的末尾添加内存屏障。问题仍然存在:在完全构造之前,您将this引用传递给其他代码(可能是其他线程)--这是不好的--,但如果唯一剩下的“构造”是注册侦听器,则对象状态与它将永远稳定。

解决方案是脆弱的,因为有一天,您或其他程序员可能需要向对象添加另一个成员,并可能忘记链接的构造函数是并发技巧,并决定在公共构造函数中初始化字段,在这样做时将在应用程序中添加难以检测到的潜在数据竞争,因此我会尝试避免使用该结构。

顺便说一下:猜测的安全性可能是错误的。我不知道编译器有多复杂/聪明,以及内存屏障(或类似物)是否是它试图优化的内容...由于构造函数是私有的,编译器确实有足够的信息来知道它只从其他构造函数中调用,这足以确定同步机制在内部构造函数中是不必要的...


编译器无法知道内部构造函数是否仅从其他构造函数中调用,因此必须确保对于仅调用内部构造函数的代码结果是正确的。非常好的想法,谢谢!除非,如果编译器检测到内部构造函数正在被另一个构造函数调用,那么它将内联到公共构造函数中而不是实际调用内部构造函数...但希望没有这样的优化 :-) - Joonas Pulakka
这就是答案:我没有考虑构造函数的内联,但这是大多数现代编译器可以执行的一种简单优化。因此,答案应该是:不要这样做。 - David Rodríguez - dribeas

1
在构造函数中逃逸对象引用可能会发布一个不完整构造的对象。即使发布是构造函数中的最后一条语句,这也是真实的。
在并发环境下,您的SafeListener可能无法正常工作,即使进行了构造函数内联(我认为没有 - 请考虑通过访问私有构造函数使用反射来创建对象)。

你的SafeListener在并发环境下可能无法正常工作,即使进行了构造函数内联。我认为SafeListener会表现得很好,这是根据Java Concurrency in Practice的建议来实现的。但是,如果执行内联操作,则MyListener肯定无法正常工作。此外,即使可以通过反射或通过封闭类的某些其他方法“原样”访问私有构造函数,也不能保证该构造函数不会被内联到调用它的其他构造函数(同一类)中。 - Joonas Pulakka

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