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

是否可以使用该对象重新排序对象访问';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冻结的最终

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

由于
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;