【问题标题】:Safe construction in java multithreadingjava多线程中的安全构造
【发布时间】:2018-01-09 15:09:10
【问题描述】:

这是在 java 多线程中的安全构造实践的上下文中。我正在阅读 JCIP 的书,有人可以在下面解释一下吗:

一个对象只有在它之后才处于可预测的、一致的状态 构造函数返回,因此从其内部发布一个对象 构造函数可以发布一个不完全构造的对象。这是 即使该出版物是 构造函数。

这里我想了解具体部分'即使出版物是 构造函数中的最后一条语句'

我知道从构造函数启动线程是不安全的,但是当它是构造函数中的最后一条语句时也不安全吗?

我的猜测是为什么它不安全是,JVM 可以对语句进行重新排序,而最后一个语句将不再是最后一个。请对此发表评论或纠正我的理解。

编辑:有问题我所说的启动线程的意思是泄漏this 引用。而且这种泄漏可能通过多种方式发生,例如发布事件或启动线程。

【问题讨论】:

  • 如果你泄漏一个过早的对象是不安全的,即你在你的构造函数中以this作为参数调用另一个对象的方法。被调用的方法已经访问了传递的引用,但是被引用的对象可能还没有完全构造。
  • 我理解这部分,我的问题是如果它是最后一个陈述。我的推理是否正确?
  • 也许您正在构造一个派生自您当前所在类的对象。在这种情况下,最终对象尚未完全构造,即使在构造函数中的最后一条语句之后也是如此。
  • 您正在阅读的引用是关于内部类的。它不适用于您给出的示例。见stackoverflow.com/questions/28676796/…
  • @yshavit 是吗?好吧,现在想一想——这是一件好事,否则到处都是一堆乱七八糟的用户引用 ;-)

标签: java multithreading


【解决方案1】:

这里有两个见解可能会对您有所帮助。首先,为了同步,构造函数(大部分)与任何其他方法一样;也就是说,它本身并不提供任何东西(除了下面提到的)。其次,线程安全始终存在于各个操作之间。

假设你有以下构造函数:

MyClass() {
    this.i = 123;
    MyClass.someStaticInstance = this; // publish the object
}

// and then somewhere else:
int value = MyClass.someStaticInstance.i;

问题是:最后一个表达式能做什么?

  • 如果 someStaticInstance 尚未设置,它会抛出 NullPointerException。
  • 也可以导致value == 123
  • 但有趣的是,它会导致value == 0

最后一点的原因是动作可以重新排序,而构造函数在这方面并不特殊。让我们仔细看看所涉及的操作:

  • A.为新实例分配空间,并将其所有字段设置为其默认值(int i 为 0)
  • 乙。设置<instance>.i = 123
  • C.设置someStaticInstance = <instance>
  • D.阅读someStaticInstance,然后是i

如果你重新排序,你可以得到:

  • A.为新实例分配空间,并将其所有字段设置为默认值(int i 为 0)
  • C.设置someStaticInstance = <instance>
  • D.阅读someStaticInstance,然后是i
  • 乙。设置<instance>.i = 123

你知道了——value 是 0,而不是 123。

JCIP 还警告您,泄漏可能会以微妙的方式发生。例如,假设您没有显式设置someStaticInstance 字段,而只是调用someListener.register(this)。你仍然泄露了引用,你应该假设你注册的监听器可能会对它做一些危险的事情,比如将它分配给someStaticInstance

即使i 字段是最终字段也是如此。您可以从 final 字段中获得一些线程安全,但前提是您不从构造函数中泄漏this。具体来说,在JLS 17.5 中,它说:

当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象的最终字段的正确初始化值。

保证“仅在对象完全初始化后才看到对该对象的引用”的唯一方法是不从其构造函数中泄漏引用。否则,您可以想象在设置字段之后,但在 JVM 将构造函数识别为已完成之前,线程读取 MyClass.someStaticInstance

【讨论】:

    【解决方案2】:

    即使它是构造函数中的最后一次调用,当您考虑继承时,它也会导致混乱的情况。下面是一些示例代码来演示该行为。

    import java.util.concurrent.TimeUnit;
    
    public class Test {
    
        public static void main(String... args) {
            A a = new B();
    
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(Thread.currentThread() + ": " + a);
        }
    
        public static void doSomethingWithA(final A a) {
            Thread t = new Thread(() -> {
                System.out.println(Thread.currentThread() + ": " + a);
            });
    
            t.start();
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    class A {
    
        public A() {
            Test.doSomethingWithA(this);
        }
    
        @Override
        protected void finalize() throws Throwable {
            if (false) {
                System.out.println(MESSAGE);
            }
        }
    }
    
    class B extends A {
        boolean isConstructed = false;
    
        public B() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.isConstructed = true;
        }
    
        @Override
        public String toString() {
            return (  "B is " + (!this.isConstructed ? "not yet " : "")
                    + "fully constructed.");
        }
    }
    

    现在,假设AB 是由两个不同的开发人员开发的。尽管A 的开发者在A 的构造函数中泄露了作为最后一个语句的早熟对象,但它不是B 的构造函数中的最后一个语句(请记住每个构造函数开头的隐式super() )。因此,在doSomethingWithA(...) 中启动的线程可以访问过早的对象。这就是为什么你应该只在构造函数中调用你自己类的final 方法的原因。更多信息,我推荐this answer

    class A 的设计存在固有缺陷,因为 this 在构造函数中的抢先泄漏,请参阅@yshavit's answer 了解更多详细信息。

    【讨论】:

    • JCIP 中讨论的问题是内存模型并没有对线程在部分构造的对象中看到的内容做出相同的保证。例如,您可以设置一个 final 字段,然后泄漏出this,并且读取该字段的不同线程不能保证读取您设置的值。
    • @pvg 我刚刚查看了 JCIP 中的部分,我不认为它专门指的是最终字段语义。它只是发出一揽子声明,警告不要从构造函数发布this——即使字段是最终的,它也没有深入解释为什么它会出现问题。
    • @yshavit 因此是“例如”。所有 JMM 保证都适用于完全构造的对象。您无需编造继承案例来解释为什么存在“最后一条语句”。
    • 我从未说过这是唯一可能发生这种情况的情况。这是一个可以用合理的代码库来解释的案例。
    【解决方案3】:

    举个例子

    class MyClass {
        public MyClass() {
            publish();
        }
        void publish() {
            System.out.println((consistent() ? "" : "in") + "consistent object");
        }
        protected boolean consistent() {
            return true;
        }
    }
    
    class Child extends MyClass {
        int result;
        public Child() {
            super();
            result = 42;
        }
        protected boolean consistent() {
            return result == 42;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-02-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-05
      • 1970-01-01
      • 2015-07-05
      • 1970-01-01
      相关资源
      最近更新 更多