是否保证 volatile 字段被正确初始化

Is it guaranteed that volatile field would be properly initialized

Here:

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

是否为 volatile 字段提供相同的保证? 如果以下示例中的 y 字段为 volatile 会怎么样?我们可以观察到 0 吗?

class FinalFieldExample { 
final int x;
int y; 
static FinalFieldExample f;

public FinalFieldExample() {
    x = 3; 
    y = 4; 
} 

static void writer() {
    f = new FinalFieldExample();
} 

static void reader() {
    if (f != null) {
        int i = f.x;  // guaranteed to see 3  
        int j = f.y;  // could see 0
    } 
} 

}

首先,volatile和初始化是不相关的概念:一个字段的初始化保证不受它是否volatile的影响。

除非this从构造函数中“逃逸”(这里不是这种情况),构造函数保证在之前任何其他进程可以完成执行访问实例的fields/methods,所以y必须在reader()中初始化if f != null,即

int j = f.y;  // will always see 4

JLS volatile

我认为读取0是可能的。

规范 says:

A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

在我们的例子中,我们对同一个变量进行了写入和读取,但是没有任何东西可以确保读取是后续的。特别是,写入和读取发生在与任何其他同步操作无关的不同线程中。

也就是说,有可能读会发生在同步顺序的写之前。

这听起来可能令人惊讶,因为写入线程在 y 之后写入 f,而读取线程仅在检测到 f 已被写入时才读取 y。但是由于对 f 的写入和读取不同步,因此以下 quote 适用:

More specifically, if two actions share a happens-before relationship, they do not necessarily have to appear to have happened in that order to any code with which they do not share a happens-before relationship. Writes in one thread that are in a data race with reads in another thread may, for example, appear to occur out of order to those reads.

explanatory notes to example 17.4.1 还重申允许运行时重新排序这些写入:

If some execution exhibited this behavior, then we would know that instruction 4 came before instruction 1, which came before instruction 2, which came before instruction 3, which came before instruction 4. This is, on the face of it, absurd.

However, compilers are allowed to reorder the instructions in either thread, when this does not affect the execution of that thread in isolation.

在我们的例子中,写入线程的行为,孤立地,不受重新排序写入 fy.

的影响

编辑:我在下面的回答看起来是错误的。 volatile 仅要求在进行写入时所有读取和写入(以及其他“操作”)已完成,但后续写入仍可重新排序以在写入 volatile 之前发生。因此可以在写入 y 之前看到 f

这真的很奇怪,但我们到了。

user17206833 在我上面的回答似乎是正确的,并且包含 link 一个非常有用的资源,我建议您检查一下。


错误的东西(我把它留下来因为它说明了一个常见的误解):

OP 我想我误解了你的问题:

“如果下面示例中的 y 字段是可变的,我们可以观察到 0 吗?”

如果 y 是不稳定的,那么不,你不能观察到 0。

class FinalFieldExample { 
  final int x;
  volatile int y; 

如果这就是你的意思,那么写入 y 之后读取 y 必须为 happens-before 创建边缘读。 The JLS says: “写入可变字段 (§8.3.1.4) 发生在该字段的每次后续读取之前。” 并且从不限定需要读取引用的语句某种类型的。 f 既不是 volatile 也不是 final 应该没有区别。

是的,当

时可以看到0
class FinalFieldExample { 
  final int x;
  volatile int y;
  static FinalFieldExample f;
  ...
}

简短说明:

  • writer() 线程通过数据竞争发布 f = new FinalFieldExample() 对象
  • 由于此数据争用,允许 reader() 线程将 f = new FinalFieldExample() 对象视为半初始化。
    特别是,reader() 线程可以看到 y = 4; 之前的 y 值——即初始值 0.

更详细的解释是here.

您可以使用 this jcstress test 在 ARM64 上重现此行为。

是的,当x是volatile时0是可能的,因为不能保证在writer()线程中写入x = 3总是 happens-beforereader() 线程中读取 local_f.x

class FinalFieldExample { 
  volatile int x;
  static FinalFieldExample f;

  public FinalFieldExample() {
    x = 3;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    var local_f = f;
    if (local_f != null) {
        int i = local_f.x;  // could see 0
    }
  }
}

因此,即使 xvolatile(这意味着对 x 的所有读取和写入都按全局顺序进行),但没有什么可以阻止读取 local_f.xreader() 线程中发生在 writer() 线程中写入 x = 3 之前。
在这些情况下,local_f.x 将 return 0intdefault value,就像初始写入一样)。

问题是在 reader() 线程读取 f 之后,无法保证(即没有 happens-before 关系)它看到f 上的内部状态正确:即它可能看不到 FinalFieldExample 构造函数中的 writer() 线程将 x = 3 写入内部字段 f.x

您可以通过以下方式创建此 happens-before 关系:

  • 要么使 f volatilex 可以成为非易失性的)
    class FinalFieldExample { 
      int x;
      static volatile FinalFieldExample f;
      ...
    }
    
    From the JLS:

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

  • 或使 x final 而不是 volatile
    class FinalFieldExample { 
      final int x;
      static FinalFieldExample f;
      ...
    }
    
    From the JLS:

    An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.