【问题标题】:Partial constructed objects in the Java Memory ModelJava 内存模型中的部分构造对象
【发布时间】:2017-08-24 09:23:24
【问题描述】:

我遇到了以下代码in an article somewhere on the Internet

public class MyInt {

    private int x;

    public MyInt(int y) {
        this.x = y;
    }

    public int getValue() {
        return this.x;
    }
}

文章指出

编译器(JIT、CPU 等)不会对构造函数进行特殊处理,因此允许对来自构造函数的指令和构造函数之后的指令重新排序。

另外,this JSR-133 article 关于 Java 内存模型指出

只有在对象完全初始化后才能看到对该对象的引用的线程,可以保证看到该对象的最终字段的正确初始化值。

上面提到的MyInt 实例似乎是不可变的(除了该类没有标记final)并且是线程安全的,但文章指出它不是。他们声明不能保证x 在读取时始终具有正确的值。

但我认为

只有创建对象的线程才能在构造对象时访问它

Java Tutorials 似乎非常支持这一点。

我的问题是:这是否意味着,对于当前的 JMM,由于指令重新排序,线程可以访问部分构造的对象?如果是,如何?这是否意味着 Java 教程中的陈述根本不正确?

【问题讨论】:

  • 另一个线程如何引用一个其构造函数尚未完成执行的对象?
  • @daniu 这也是我想知道的事情,因此我很困惑。

标签: java concurrency final java-memory-model


【解决方案1】:

那篇文章说如果你有类似的代码

foo = new MyInt(7);

在具有字段的类中

MyInt foo;

然后是指令

(reference to new object).x = 7;
foo = (reference to new object);

可以作为某种优化进行交换。这永远不会改变运行此代码的线程的行为,但其他线程可能会在该行之后读取foo

foo = (reference to new object);

但在行前

(reference to new object).x = 7;

在这种情况下,它会将foo.x 视为0,而不是7。也就是说,其他线程可以运行

int bar = someObject.getFoo().getValue();

最后得到bar 等于0

我从来没有见过这样的事情发生在野外,但作者似乎知道他在说什么。

【讨论】:

  • 我很确定答案在于编译器规范/实现的细节。虽然它通常可能“允许重新排序来自构造函数的指令和构造函数之后的指令”,但我很确定该对象的分配不算作交换的有效指令。
【解决方案2】:

单独的指令重新排序不能导致另一个线程看到部分构造的对象。根据定义,只有在不影响正确同步程序的行为的情况下,JVM 才被允许重新排序。

对象引用的不安全发布会导致坏事发生。例如,这是一个特别糟糕的单例尝试:

public class BadSingleton {
   public static BadSingleton theInstance;

   private int foo;

   public BadSingleton() {
      this.foo = 42;
      if (theInstance == null) {
         theInstance = this;
      }
   }
}

在这里,您不小心在static 字段中发布了对正在构造的对象的引用。在 JVM 决定重新排序事物并在分配给 theInstance 之后放置 this.foo = 42 之前,这不一定是一个问题。因此,这两件事共同破坏了您的不变量,并允许另一个线程看到 BadSingleton.theInstancefoo 字段未初始化。

意外发布的另一个常见来源是从构造函数调用可覆盖的方法。这并不总是导致意外发布,但潜力是存在的,因此应该避免。

只有创建对象的线程才能在构造对象时访问它

这是否意味着 Java 教程中的声明是 根本不正确?

是和不是。这取决于我们如何解释 should 这个词。不能保证在所有可能的情况下,另一个线程不会看到部分构造的对象。但从某种意义上说,您确实应该编写不允许这种情况发生的代码。

【讨论】:

  • “指令重新排序不能导致另一个线程看到部分构造的对象。”当然可以,如达伍德的回答所示。您给出的代码示例与此无关,是构造不当的示例。
  • @Oleg 错误,他的示例仅在对对象的引用不安全发布时才有效。如果没有不安全的发布,其他线程就无法看到部分构造的对象。反之亦然,对于不安全的发布,您不需要重新排序指令即可查看部分构造的对象。因此指令重新排序自己不会导致看到部分构造的对象。
  • 当然,但是指令重新排序可以在不安全地发布时单独导致问题。这就像说,如果你不挡路,子弹就不会杀死你。很明显,需要不安全地发布对象才能存在问题,这里描述的问题是由指令重新排序引起的。
  • 好吧,无论如何我删除了我的反对票。我对同一个问题的最后一句话是,是的,当对象被不安全地发布时,它会自行产生问题,我同意你们其他人的评论,我们相互理解并且主要在语义上争论。
  • @Oleg 谢谢你,我重新阅读了我的答案,我想我现在更好地理解了你的担忧。我已经改变了这个例子来演示这两个东西究竟是如何相互作用和搞砸的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-14
  • 2014-01-08
  • 2016-07-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多