Java 对象引用的不当发布

2022-09-01 17:45:42

下面的例子来自Brian Goetz的“Java并发实践”一书,第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");
  }
}

它说持有者可能出现在另一个线程的不一致状态,而另一个线程可以观察部分构造的对象。怎么会这样?你能用上面的例子给出一个场景吗?

此外,它还继续说,在某些情况下,线程可能会在第一次读取字段时看到过时的值,然后在下次看到更新的值,这就是为什么可以抛出 .如何投掷?assertSanityAssertionErrorAssertionError

从进一步的阅读来看,解决此问题的一种方法是通过使变量最终确定来使不可变。现在,让我们假设它不是一成不变的,而是实际上是不可变的。HoldernHolder

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

像这样:

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

答案 1

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

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

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

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

holder可能不是空的,但可以是空的,也可以是0。someStringsomeInt

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

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

因此,如果 是易失性的,并且您看到的不为 null,则基于易失性规则,所有字段都将被初始化。holderholder

为了安全地发布此对象,我们是否必须使持有者初始化成为静态的并将其声明为易失性

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

如何投掷?AssertionError

如果线程注意到不为 null,并且在输入方法时调用,并且第一次读取可能是(默认值),则第二次读取 现在可能会看到来自第一个线程的写入。holderAssertionErrorn0n


答案 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");
  }
}

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

构造函数中的 赋值发生在一个线程中。并且在另一个线程中发生两次读取。这里唯一的发生之前关系是两次读取之间。不存在涉及赋值和任何读取的发生前关系。this.nn

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

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


推荐