以下代码示例取自 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 != null
和f.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 = f
和i = local.x
以解引用链关系连接,这给了我们开头提到的整个链:
hb(w, f) -> hb(f, a) -> mc(a, r1) -> dereferences(r1, r2)
关于java - 在 Java 中,对象访问可以用该对象的最终字段访问重新排序吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62278680/