如果使用 volatile 引用发布,另一个线程是否可以看到处于不一致状态的有效不可变对象?

Can another thread see an effectively immutable object in an inconsistent state if it is published with a volatile reference?

根据 Java Concurrency in Action 如果我们有以下 class:

public class Wrapper {
  private int num;

  public Wrapper(int num) {
    this.num = num;
  }

  public void assertCorrectness() {
    if (num != num)
      throw new AssertionError("This is false");
  }
}

并且我们初始化这个 class 的实例并以不安全的方式发布它(例如通过一个简单的 public 字段),然后 assertCorrectness() 可能确实会抛出 AssertionError ,如果从另一个线程调用。换句话说,这意味着另一个线程可能会看到对实例的最新引用,但实例本身的状态可能已过时(因此线程可以看到一个对象存在但它处于部分构造/不一致的状态)。

另一方面,据说通过 volatile 引用发布此 class 的实例被认为是安全的。然而,我的理解是 volatile 只是保证任何线程将始终看到引用的最新版本,而不是被引用的对象的状态。所以我们可以确定,如果一个线程将 Wrapper class 的新实例分配给一个 volatile 字段,那么所有其他线程都会看到该引用已更新。但是他们是否仍然会看到处于不一致/部分构造状态的对象的风险?

否,因为 volatile 的使用建立了 happens-before 关系。如果没有它,各种重新排序和其他事情是允许的,这使得不一致的状态成为可能,但是有了它,JVM 必须给你预期的结果。

在这种情况下,volatile 不是 用于可见性效果(线程看到最新值),但 [=20= 提供的安全发布]. volatile 的这个特性在解释它的使用时经常被忽略。

正确。

请记住 effectively immutable + safe publication 在某些情况下的行为不符合直觉。
例如:

  1. 如果

    • 首先thread 1安全地将对象o发布到thread 2
    • 然后 thread 2 不安全地将对象 o 发布到 thread 3

    最后thread 3可以看到对象o处于不一致的状态
    参见 [1] and

  2. 还有this

真正的不可变对象没有这样的问题。

很简单,但很棒。 volatile 很少被解释和用于安全发布,但它提供了所需的保证。

恕我直言,为了正确证明这适用于 volatile,有些事情需要解释。

第一个是“程序顺序”,或者线程内事情发生的可感知顺序。我们可以画这个更容易掌握:

 --------------- T1 -------------
 | write to num or this.num=num |
 --------------------------------
                 |
                \|/  (PO)
 --------------- T1 -------------
 |    write Wrapper instance    |
 --------------------------------
                 |
                \|/  (??)
 --------------- T2 -------------
 |    read Wrapper instance     |
 --------------------------------
                 |
                \|/  (PO)
 --------------- T2 -------------
 |      read num first time     |
 --------------------------------
                 |
                \|/  (PO)
 --------------- T2 -------------
 |      read num second time    |
 --------------------------------

T1T2是线程1和线程2,PO是程序顺序。现在 JLS 中的一条规则是这样说的:

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

因此我们可以将上图中的PO替换为HB (happens-before).

同时,如果wrapper实例不是volatile,wrapper(来自 T1)和 阅读 来自 T2 中的 wrapper

这里还要介绍一下happen-before一致性:

a read sees the last write in happens-before order, or any other write.

因为我们这里没有完整的 happens-before 链(?? 没有建立),我们得到:“...或任何其他读取”,这意味着我们可以在 num 的那两次读取中读取 不同的 值(其中 num != num)。


如果你创建 wrapper 实例 volatileJLS 表示:

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

因此,我们现在有了这个:

 --------------- T1 -------------
 | write to num or this.num=num |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T1 -------------
 |    write Wrapper instance    |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T2 -------------
 |    read Wrapper instance     |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T2 -------------
 |      read num first time     |
 --------------------------------
                 |
                \|/  (HB)
 --------------- T2 -------------
 |      read num second time    |
 --------------------------------

现在,happens-before 一致性表示:“read 看到 happens-before 顺序中的最后一个 write。 ..";这里的关键部分是“按 happens-before 顺序”。这意味着 both 读取 num 将看到它的写入(再次:在顺序之前发生)。