是否可以使用 Java 中该对象的最终字段访问对对象访问进行重新排序?

Can object access be reordered with that object's final field access in Java?

以下代码示例取自 JLS 17.5 "final Field Semantics":

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
        } 
    } 
}

由于 FinalFieldExample 的实例是通过数据竞争发布的,是否有可能 f != null 检查评估成功,但随后的 f.x 取消引用将 f 视为null?

换句话说,有没有可能得到一个注释为"guaranteed to see 3"的NullPointerException在线?

好的,这是我自己的看法,基于相当详细的 talk (in Russian) on final semantics given by Vladimir Sitnikov, and subsequent revisit of JLS 17.5.1

最终字段语义

规范指出:

Given a write w, a freeze f, an action a (that is not a read of a final field), a read r1 of the final field frozen by f, and a read r2 such that hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2), then when determining which values can be seen by r2, we consider hb(w, r2).

换句话说,如果可以建立以下关系链,我们就可以保证看到对 final 字段的写入:

hb(w, f) -> hb(f, a) -> mc(a, r1) -> dereferences(r1, r2)


1。 hb(w, f)

w 是写入最后一个字段:x = 3

f 是 "freeze" 操作(退出 FinalFieldExample 构造函数):

Let o be an object, and c be a constructor for o in which a final field f is written. A freeze action on final field f of o takes place when c exits, either normally or abruptly.

由于字段写入在程序顺序中完成构造函数之前进行,我们可以假设 hb(w, f):

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

2。 hb(f, a)

规范中给出的a的定义实在含糊("action, that is not a read of a final field")

我们可以假设 a 正在发布对对象 (f = new FinalFieldExample()) 的引用,因为这个假设与规范不矛盾(它是一个动作,而不是读取最后一个字段)

由于在程序顺序中完成构造函数先于编写引用,因此这两个操作按发生前关系排序:hb(f, a)

3。 mc(a, r1)

在我们的例子中 r1 是一个 "read of the final field frozen by f" (f.x)

这就是它开始变得有趣的地方。 mc(内存链)是 "Semantics of final Fields" 部分中引入的两个附加偏序之一:

There are several constraints on the memory chain ordering:

  • If r is a read that sees a write w, then it must be the case that mc(w, r).
  • If r and a are actions such that dereferences(r, a), then it must be the case that mc(r, a).
  • If w is a write of the address of an object o by a thread t that did not initialize o, then there must exist some read r by thread t that sees the address of o such that mc(r, w).

对于问题中给出的简单示例,我们实际上只对第一点感兴趣,因为需要其他两点来推理更复杂的情况。

下面是真正解释为什么有可能获得 NPE 的部分:

  • 注意规范引用中的粗体部分:mc(a, r1) 关系仅存在 if 字段的读取看到对共享引用的写入
  • f != nullf.x 从 JMM 的角度来看是两个不同的读取操作
  • 规范中没有任何内容表明 mc 关系在程序顺序或发生之前是可传递的
  • 因此,如果 f != null 看到另一个线程完成的写入,则无法保证 f.x 也看到它

我不会详细介绍取消引用链约束,因为它们只需要推理较长的引用链(例如,当最终字段引用一个对象时,该对象又引用另一个对象)。

对于我们的简单示例,只需说明 JLS 声明 "dereferences order is reflexive, and r1 can be the same as r2"(这正是我们的情况)。

处理不安全发布的安全方法

下面是保证不会抛出 NPE 的代码修改版本:

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() {
        FinalFieldExample local = f;
        if (local != null) {
            int i = local.x;  // guaranteed to see 3  
            int j = local.y;  // could see 0
        } 
    } 
}

这里的重要区别是将共享引用读入局部变量。 正如 JLS 所述:

Local variables ... are never shared between threads and are unaffected by the memory model.

因此,从 JMM 的角度来看,只有一个共享状态读取。

如果读操作恰好看到另一个线程完成的写操作,则意味着这两个操作是连接内存链(mc)的关系。 此外,local = fi = local.x 以解引用链关系连接,这给了我们开头提到的整个链:

hb(w, f) -> hb(f, a) -> mc(a, r1) -> dereferences(r1, r2)

你的分析很漂亮 (1+),如果我能投票两次 - 我会的。这里还有一个 link 与 "independent reads" here, for example.

相同的问题

我也尝试过解决这个问题in a different answer too

我想如果我们在这里引入相同的概念,那么事情也可以被证明。让我们采用该方法并稍微更改一下:

static void reader() {

    FinalFieldExample instance1 = f;

    if (instance1 != null) {

        FinalFieldExample instance2 = f;
        int i = instance2.x;    

        FinalFieldExample instance3 = f;
        int j = instance3.y;  
    } 
}

并且编译器现在可以进行一些急切的读取(将这些读取 移动到 之前 if statement):

static void reader() {

    FinalFieldExample instance1 = f;
    FinalFieldExample instance2 = f;
    FinalFieldExample instance3 = f;

    if (instance1 != null) {
        int i = instance2.x;    
        int j = instance3.y;  
    } 
}

这些读取可以在它们之间进一步重新排序:

static void reader() {

    FinalFieldExample instance2 = f;
    FinalFieldExample instance1 = f;
    FinalFieldExample instance3 = f;

    if (instance1 != null) {
        int i = instance2.x;    
        int j = instance3.y;  
    } 
}

从这里开始事情应该是微不足道的:ThreadA 读取 FinalFieldExample instance2 = f;nullbefore 它执行下一个读取:FinalFieldExample instance1 = f; 一些 ThreadB 调用 writer(因此 f != null)和部分:

 FinalFieldExample instance1 = f;

解析为 non-null