是否可以使用该对象重新排序对象访问';Java中的最终字段访问?
以下代码示例取自JLS 17.5“最终字段语义”: 由于是否可以使用该对象重新排序对象访问';Java中的最终字段访问?,java,final,java-memory-model,instruction-reordering,Java,Final,Java Memory Model,Instruction Reordering,以下代码示例取自JLS 17.5“最终字段语义”: 由于FinalFieldExample的实例是通过数据竞争发布的,因此f!=空检查计算成功,但随后的f.x取消引用将f视为空 换句话说,是否有可能在一行中得到一个注释为“保证看到3”的NullPointerException,好的,根据弗拉基米尔·西特尼科夫(Vladimir Sitnikov)给出的非常详细的(俄语)最终语义,以及随后对的重新访问, 最终字段语义 该规范规定: 给定写入w、冻结f、动作a(不是读取最终字段)、读取被f冻结的最终
FinalFieldExample
的实例是通过数据竞争发布的,因此f!=空
检查计算成功,但随后的f.x
取消引用将f
视为空
换句话说,是否有可能在一行中得到一个注释为“保证看到3”的
NullPointerException
,好的,根据弗拉基米尔·西特尼科夫(Vladimir Sitnikov)给出的非常详细的(俄语)最终语义,以及随后对的重新访问,最终字段语义 该规范规定: 给定写入w、冻结f、动作a(不是读取最终字段)、读取被f冻结的最终字段的r1、读取r2,使得hb(w,f)、hb(f,a)、mc(a,r1)和解引用(r1,r2),然后在确定r2可以看到哪些值时,我们考虑Hb(W,R2)。 换句话说,如果可以建立以下关系链,我们可以保证看到写入到最终字段:
hb(w, f) -> hb(f, a) -> mc(a, r1) -> dereferences(r1, r2)
1.血红蛋白(w,f) w是对最后一个字段的写入:
x=3
f是“冻结”操作(退出
FinalFieldExample
constructor):
设o为对象,c为o的构造函数,其中
写入字段f。在o的最后一个字段f上发生冻结操作
当c退出时,通常或突然退出
由于字段写入是在按程序顺序完成构造函数之前进行的,我们可以假设hb(w,f)
:
如果x和y是同一线程的动作,并且x在程序顺序中位于y之前,那么hb(x,y)
2.血红蛋白(f,a)
规范中给定字段的定义非常模糊(“操作,这不是对最终字段的读取”)我们可以假设a正在发布对对象的引用(
f=new FinalFieldExample()
),因为此假设与规范不矛盾(它是一个操作,并且不是对最终字段的读取)由于完成构造函数是在按程序顺序编写引用之前进行的,因此这两个操作是按“发生在”关系排序的:
hb(f,a)
3.mc(a,r1)
在我们的例子中,r1是“读取被f冻结的最终字段”(f.x
)这就是它开始变得有趣的地方。mc(内存链)是“最终字段语义”一节中介绍的两个附加偏序之一: 内存链排序有几个限制:
- 如果r是一个读操作,它会看到一个写操作w,那么mc(w,r)必须是这种情况
- 如果r和a是解引用(r,a)的动作,那么它必须是mc(r,a)的情况
- 如果w是一个没有初始化o的线程t对对象o的地址的写入,那么一定存在一些由线程t读取的r,它看到o的地址,使得mc(r,w)
下面的部分实际上解释了为什么可以获得NPE:
- 请注意spec quote中的粗体部分:
关系仅在字段的读取看到对共享引用的写入时才存在mc(a,r1)
f!=从JMM的角度来看,null和f.x是两种不同的读取操作
- 规范中没有任何内容表明,
关系相对于程序顺序是可传递的,或者之前发生过mc
- 因此,如果
看到由另一个线程完成的写入,不能保证f!=null
也看到它f.x
对于我们的简单示例,只需说JLS声明“解引用顺序是自反的,r1可以与r2相同”(这正是我们的情况) 处理不安全出版物的安全方法 以下是保证不会抛出NPE的代码的修改版本:
class FinalFieldExample{
最终整数x;
int-y;
静态最终字段样本f;
公共财政字段示例(){
x=3;
y=4;
}
静态void writer(){
f=新的FinalFieldExample();
}
静态无效读取器(){
FinalFieldExample local=f;
如果(本地!=null){
int i=local.x;//保证看到3
int j=local.y;//无法看到0
}
}
}
这里的重要区别是将共享引用读入局部变量。
如JLS所述:
局部变量。。。从不在线程之间共享,并且不受内存模型的影响
因此,从JMM的角度来看,只有一次从共享状态读取。如果该读取碰巧看到另一个线程完成了写入,则意味着这两个操作通过内存链(
mc
)关系连接。
此外,local=f
和i=local.x
与解引用链关系相连接,这为我们提供了开头提到的整个链:
hb(w, f) -> hb(f, a) -> mc(a, r1) -> dereferences(r1, r2)
你的分析很漂亮(1+),如果我能投两次赞成票,我会的。这里还有一个链接指向“独立阅读”的相同问题 我也试图解决这个问题 我认为如果我们在这里引入同样的概念,事情也可以证明。让我们采用该方法并稍微更改它:
static void reader() {
FinalFieldExample instance1 = f;
if (instance1 != null) {
FinalFieldExample instance2 = f;
int i = instance2.x;
FinalFieldExample instance3 = f;
int j = instance3.y;
}
}
编译器现在可以执行一些紧急读取(将这些读取移到if状态之前)
static void reader() {
FinalFieldExample instance1 = f;
if (instance1 != null) {
FinalFieldExample instance2 = f;
int i = instance2.x;
FinalFieldExample instance3 = f;
int j = instance3.y;
}
}
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;
}
}
FinalFieldExample instance1 = f;