Java Object Reference的发布不正确

时间:2013-04-19 15:04:24

标签: java concurrency

以下示例来自Brian Goetz,第3章,第3.5.1节中的“Java Concurrency in Practice”一书。这是不正确的对象发布的一个例子

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可以抛出断言错误的原因。如何抛出assertionError?

从进一步阅读,解决这个问题的一种方法是通过使变量'n'最终使Holder不可变。现在,让我们假设持有人不是无法忍受的,而是有效的不可改变的。为了安全地发布这个对象,我们是否必须使holder初始化为静态并将其声明为volatile(静态初始化和volatile或者只是volatile)?像

这样的东西
public class someClass {
    public static volatile Holder holder = new Holder(42);

}

提前感谢您的帮助。

4 个答案:

答案 0 :(得分:12)

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

holder中写holder字段之前,JMM没有编写和发布initialie()成员字段的规则。这意味着即使holder不为null,成员字段对其他线程仍然不可见是合法的。

你最终可能会看到类似

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

holder可能不为null,但someString可能为null,someInt可能为0.

在x86拱门下,据我所知,这是不可能发生的,但在其他情况下可能并非如此。

所以下一个问题可能是Why does volatile fix this? JMM说在volatile列表之前发生的所有写操作都对volatile字段的所有后续线程都可见。

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

  

为了安全地发布此对象,我们是否必须制作持有者   初始化静态并将其声明为volatile

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

  

如何抛出assertionError?

如果一个帖子注意到holder不为空,并在输入方法时调用assertionError并且第一次阅读n可能是0(默认值) ,n的第二次读取现在可以看到第一个帖子的写入。

答案 1 :(得分: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的读取。这里唯一发生的关系是两次读取之间的关系。没有发生过涉及任务和任何阅读的关系。

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

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

答案 2 :(得分:0)

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

MyClass obj = new MyClass()

并不总是按步骤完成:

  1. 为堆上的MyClass新实例保留内存
  2. 执行构造函数以设置内部属性值
  3. 在堆上设置'obj'对地址的引用
  4. 出于某些优化目的,JVM可以通过以下步骤完成:

    1. 为堆上的MyClass新实例保留内存
    2. 在堆上设置'obj'对地址的引用
    3. 执行构造函数以设置内部属性值
    4. 因此,想象一下两个线程是否想要访问MyClass对象。第一个创建它但由于JVM它执行'优化'步骤集。如果它只执行步骤1和2(但不会执行3),那么我们就会遇到严重的问题。如果第二个线程使用这个对象(它不会为null,因为它已经指向堆上的内存的保留部分),它的属性将是不正确的,这可能导致令人讨厌的事情。

      如果引用不稳定,则不会发生这种优化。

答案 3 :(得分:-1)

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