是否可以使用 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 != null
和 f.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 = f
和 i = 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;
为 null
,before 它执行下一个读取:FinalFieldExample instance1 = f;
一些 ThreadB
调用 writer
(因此 f != null
)和部分:
FinalFieldExample instance1 = f;
解析为 non-null
。
以下代码示例取自 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 != null
和f.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 = f
和 i = 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;
为 null
,before 它执行下一个读取:FinalFieldExample instance1 = f;
一些 ThreadB
调用 writer
(因此 f != null
)和部分:
FinalFieldExample instance1 = f;
解析为 non-null
。