代码之家  ›  专栏  ›  技术社区  ›  Corey Ross

在链接构造函数时,jvm的隐式内存屏障是如何工作的?

  •  14
  • Corey Ross  · 技术社区  · 7 年前

    指的是我的 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() 保证实际 呼叫 上面的私有构造函数(在这种情况下,在预期的地方会有障碍,一切都是安全的),或者私有构造函数是否可能 内联 作为保存一个内存障碍的优化(在这种情况下,在公共构造函数结束之前不会有障碍)?

    规则是 这个() 准确的定义在什么地方?如果不允许,那么我认为我们必须假设允许内联链式构造函数,并且可能允许一些jvm甚至 javac S正在做。

    5 回复  |  直到 14 年前
        1
  •  6
  •   Giambattista Bloisi    14 年前

    我认为它是安全的,因为Java内存模型指出:

    o 成为一个对象,并且 C 成为 o 其中一个决赛 领域 f 是书面的。终场冻结动作 f 属于 o 发生 什么时候 C 正常或突然退出。注意,如果 构造函数调用另一个构造函数,被调用的构造函数 设置一个final字段,final字段的冻结发生在 调用的构造函数结束。

        2
  •  3
  •   Thomas Jung    16 年前

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

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

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

        3
  •  3
  •   Brian Goetz    16 年前

    您的第二个版本不正确,因为它允许“this”引用从构造过程中转义。使用“this”转义将使赋予最终字段安全性的初始化安全保证失效。

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

        4
  •  1
  •   David Rodríguez - dribeas    16 年前

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


    我猜它会很脆弱但没问题。编译器不知道是否只从其他构造函数中调用内部构造函数,因此必须确保只调用内部构造函数的代码的结果正确,所以不管它使用什么机制(内存屏障?)必须在那里。

    我猜编译器会在每个构造函数的末尾添加内存屏障。问题仍然存在:你通过了 this 在完全构造之前对其他代码(可能是其他线程)的引用——这是不好的——但是如果剩下的唯一构造是注册侦听器,那么对象状态将一如既往地稳定。

    解决办法是 脆弱的 在另一天,您或其他程序员可能需要向对象中添加另一个成员,并且可能会忘记链式构造函数是一种并发技巧,并可能决定在公共构造函数中初始化字段,这样做将在yo中添加一个难以检测的潜在数据竞争。你的应用程序,所以我会尽量避免那个构造。

    顺便说一句:猜测的安全性可能是错误的。我不知道编译器有多复杂/智能,也不知道它是否可以尝试优化内存障碍(或类似的东西)。由于构造函数是私有的,编译器确实有足够的信息来知道它只是从其他构造函数调用的,并且这足够的信息来确定内部构造函数中不需要同步机制…

        5
  •  1
  •   Ovidiu Lupas    16 年前

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

    即使执行了C-Tor内联(我认为这不是——考虑通过访问私有C-Tor来使用反射创建对象),在并发环境中,您的安全侦听器的行为也可能不正常。