java - 在 Java 中,对象访问可以用该对象的最终字段访问重新排序吗?

标签 java final java-memory-model instruction-reordering

以下代码示例取自 JLS 17.5“最终字段语义”:

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 (俄语)关于 Vladimir Sitnikov 给出的最终语义,以及随后对 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 是“卡住”操作(退出 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 定义非常模糊(“操作,不是读取 final 字段”)

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

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

3。 mc(a, r1)

在我们的例子中,r1 是“读取由 f 卡住的最终字段”(f.x)

这就是它开始变得有趣的地方。 mc(内存链)是“final 字段的语义”部分介绍的两个额外的偏序之一:

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

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

对于我们的简单示例,只需说明 JLS 声明“取消引用顺序是自反的,并且 r1 可以与 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)

关于java - 在 Java 中,对象访问可以用该对象的最终字段访问重新排序吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62278680/

相关文章:

Java,从带有特殊符号的txt文件中读取数字到二维整数数组中

java - 我的 RecyclerView 中的 SQLite 数据没有被删除

java - Oracle 应用服务器上每个 Java VM 的单独 GC 日志文件

java - 在 Android 中使用 TimerTask

constructor - 如何在构造函数中初始化最终类属性?

java - 连续为多个可变字段赋值的操作是否可以重新排序?

java - 最终实例变量的安全发布是否可传递给非最终二级引用?

java - 如何在 FXML (JavaFX) 中为按钮定义列

java - 如何从 Java 中的多个 final 字符串中随机选择一个字符串?

Scala 和 Java 内存模型