【问题标题】:Improper publication of Java Object ReferenceJava Object Reference 发布不当
【发布时间】:2013-04-13 00:39:51
【问题描述】:

以下示例来自 Brian Goetz 的《Java Concurrency in Practice》一书,第 3 章,第 3.5.1 节。这是对象发布不当的一个例子:

class SomeClass {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n != n)
      throw new AssertionError("This statement is false");
  }
}

它表示 Holder 可能会以不一致的状态出现在另一个线程中,并且另一个线程可以观察到部分构造的对象。这怎么可能发生?你能用上面的例子给出一个场景吗?

还继续说,在某些情况下,线程第一次读取字段时可能会看到一个陈旧的值,然后在下一次读取一个更新的值,这就是为什么 assertSanity 可以抛出 @ 987654325@。 AssertionError怎么扔?

通过进一步阅读,解决此问题的一种方法是通过将变量 n 设为 final 来使 Holder 不可变。现在,让我们假设Holder 不是不可变的,但实际上是不可变的。

为了安全地发布此对象,我们是否必须将持有者初始化设为静态并将其声明为 volatile(静态初始化和 volatile 或只是 volatile)?

类似这样的:

public class SomeClass {
    public static volatile Holder holder = new Holder(42);
}

【问题讨论】:

  • 我所能看到的只是在多处理器情况下两个处理器之间缓存状态不一致的可能性。在非紧密耦合的 MP 环境中,这始终是可能的,除非您采取明确的同步步骤。
  • @PaulGrime - 经过简短的审查后,我没有看到任何可以解决上述情况的内容。在构造对象之前,引用不会“转义”。 int n 不公开,不能在课堂外查看。
  • 不,但holder 是公共的,另一个线程可能会在其构造函数和字段初始化完成之前调用assertSanity - stackoverflow.com/a/4926812/319878。也许 - stackoverflow.com/a/10528614/319878.
  • @PaulGrime - 在构造函数返回之前(如果 JITC 兼容),对创建对象的引用不会分配给 holder
  • 这些字段需要是 final 或 volatile 以保证它们在构造函数返回后完全初始化。

标签: java concurrency


【解决方案1】:

你可以想象一个对象的创建有许多非原子函数。首先你要初始化并发布 Holder。但是您还需要初始化所有私有成员字段并发布它们。

嗯,JMM 没有规则让 holder 的成员字段的写入和发布发生在 holder 字段的写入之前发生在 initialize() 中。这意味着即使holder 不为null,成员字段对其他线程不可见也是合法的。

你最终可能会看到类似的东西

public class Holder {
    String someString = "foo";
    int someInt = 10;
}

holder 可能不为空,但someString 可能为空,someInt 可能为 0。

在 x86 架构下,据我所知,这是不可能发生的,但在其他架构中可能并非如此。

所以下一个问题可能是“为什么 volatile 会解决这个问题?”JMM 表示,在 volatile 存储之前发生的所有写入对 volatile 字段的所有后续线程都是可见的。

因此,如果 holder 是 volatile 并且您看到 holder 不为 null,则根据 volatile 规则,所有字段都将被初始化。

为了安全地发布这个对象,我们是否必须让 holder 初始化 static 并将其声明为 volatile

是的,因为正如我所提到的,如果 holder 变量不为空,那么所有写入都是可见的。

AssertionError 怎么扔?

如果线程注意到holder不为空,进入方法时调用AssertionError,第一次读取n可能是0(默认值),第二次读取n现在可能会看到来自第一个线程的写入。

【讨论】:

  • 感谢您的详细回答,这很有意义。那么您是说持有人需要同时具有 volatile 和静态初始化,还是仅 volatile 就足够了?
  • 在没有 volatile 的情况下,内联静态初始化实际上是可以的(如果是这种情况,只需将其设为 final),这是因为类初始化(而不是对象发布)发生在可以使用类之前。如果它不是静态的,那么volatile + 非空检查就足够了。
  • 详细说明this is because class initialization (not object publication) happens-before the class can be used 在类完全初始化之前发生的所有写入一旦类能够被使用,对线程都是可见的。
【解决方案2】:
public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n!=n)
      throw new AssertionError("This statement is false");
  }
}

假设一个线程创建了一个Holder 的实例,并将引用传递给另一个调用assertSanity 的线程。

在构造函数中对this.n 的赋值发生在一个线程中。 n 的两次读取发生在另一个线程中。这里唯一的发生前关系是两个读取之间的关系。没有涉及分配和任何读取的发生之前的关系。

没有任何happens-before关系,语句可以以各种方式重新排序,因此从一个线程的角度来看,this.n = n可以在构造函数返回之后发生。

这意味着赋值可能出现在第一次读取之后和第二次读取之前的第二个线程中,从而导致值不一致。可以通过将 n 设为 final 来防止这种情况发生,这可以保证在构造函数完成之前分配值。

【讨论】:

    【解决方案3】:

    你问的问题是由JVM优化和简单的对象创建引起的:

    MyClass obj = new MyClass()
    

    并不总是按步骤完成:

    1. 在堆上为 MyClass 的新实例保留内存
    2. 执行构造函数设置内部属性值
    3. 将“obj”引用设置为堆上的地址

    出于某些优化目的,JVM 可以分步进行:

    1. 在堆上为 MyClass 的新实例保留内存
    2. 将“obj”引用设置为堆上的地址
    3. 执行构造函数设置内部属性值

    所以,想象一下如果两个线程想要访问 MyClass 对象。第一个创建它,但由于 JVM,它执行“优化”的一组步骤。如果它只执行第 1 步和第 2 步(但不会执行第 3 步),那么我们可能会遇到严重的问题。如果第二个线程使用这个对象(它不会为空,因为它已经指向堆上保留的内存部分),那么它的属性将不正确,这可能会导致令人讨厌的事情。

    如果引用是可变的,则不会发生这种优化。

    【讨论】:

      【解决方案4】:

      Holder 类没问题,但 someClass 类可能会出现不一致的状态 - 在创建和调用 initialize() 之间,holder 实例变量是 null

      【讨论】:

      • 是的,直到;但我不能对自己的帖子投反对票,也不想删除它……哦,好吧。
      猜你喜欢
      • 2013-01-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-15
      • 1970-01-01
      • 1970-01-01
      • 2021-10-13
      • 2019-09-26
      相关资源
      最近更新 更多