用反射打破JIT优化


9

当为高度并发的单例类研究单元测试时,我偶然发现了以下奇怪的行为(在JDK 1.8.0_162上进行了测试):

private static class SingletonClass {
    static final SingletonClass INSTANCE = new SingletonClass(0);
    final int value;

    static SingletonClass getInstance() {
        return INSTANCE;
    }

    SingletonClass(int value) {
        this.value = value;
    }
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

    System.out.println(SingletonClass.getInstance().value); // 0

    // Change the instance to a new one with value 1
    setSingletonInstance(new SingletonClass(1));
    System.out.println(SingletonClass.getInstance().value); // 1

    // Call getInstance() enough times to trigger JIT optimizations
    for(int i=0;i<100_000;++i){
        SingletonClass.getInstance();
    }

    System.out.println(SingletonClass.getInstance().value); // 1

    setSingletonInstance(new SingletonClass(2));
    System.out.println(SingletonClass.INSTANCE.value); // 2
    System.out.println(SingletonClass.getInstance().value); // 1 (2 expected)
}

private static void setSingletonInstance(SingletonClass newInstance) throws NoSuchFieldException, IllegalAccessException {
    // Get the INSTANCE field and make it accessible
    Field field = SingletonClass.class.getDeclaredField("INSTANCE");
    field.setAccessible(true);

    // Remove the final modifier
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    // Set new value
    field.set(null, newInstance);
}

main()方法的最后两行在INSTANCE的值上存在分歧-我猜是JIT完全放弃了该方法,因为该字段是static final。删除final关键字会使代码输出正确的值。

抛开对单身人士的同情(或缺乏同情心),忘记一分钟使用这样的反射会带来麻烦-我的假设是正确的,因为应该归咎于JIT优化?如果是这样-那些字段仅限于静态最终字段吗?


1
单例是只能存在一个实例的类。因此,您没有单身人士,只有一个带有static final字段的类。除此之外,此反射hack是否由于JIT或并发而中断也没关系。
霍尔格

@Holger,此hack是在单元测试中完成的,仅是为了尝试模拟使用该类的多个测试用例的单例。我不知道并发是怎么引起的(上面的代码中没有),我真的很想知道发生了什么。
凯尔姆

1
好吧,您在问题中说“高度并行的单例课程”,而我说“ 没关系 ”是什么让它中断了。因此,如果您的特定示例代码由于JIT而中断,并且您找到了解决方法,然后实际代码从因JIT而中断变为因并发而中断,您获得了什么?
霍尔格

@Holger好吧,那里的措辞有点太强烈了,对此感到抱歉。我的意思是-如果我们不明白为什么某件事出了这么大的错误,将来我们很容易被同一件事所咬,所以我宁愿知道原因,也不愿假设“事情只是发生了”。无论如何,感谢您抽出宝贵的时间回答!
Kelm

Answers:


7

从字面上回答您的问题,“ …我的假设正确无误,因为应该归咎于JIT优化?”,答案是肯定的,在此特定示例中,很可能是JIT优化导致了此行为。

但是因为改变 static final字段完全超出规范,因此还有其他一些事情可以类似地破坏它。例如,JMM没有定义此类更改的内存可见性,因此,完全不确定是否或何时其他线程注意到此类更改。他们甚至不需要一致地注意到它,即,即使存在同步原语,他们也可以使用新值,然后再次使用旧值。

不过,无论如何,JMM和优化器在这里很难分开。

您的问题“ …仅限于静态最终字段吗?“”很难回答,因为优化当然不限于static final字段,但非静态final字段的行为也不相同,并且在理论和实践之间也存在差异。

对于非静态final字段,在某些情况下允许通过反射进行修改。这表明事实setAccessible(true)足以使这种修改成为可能,而无需侵入Field实例来更改内部modifiers字段。

规格说明:

17.5.3。随后的final字段修改

在某些情况下,例如反序列化,系统将需要final在构造后更改对象的字段。final可以通过反射和其他依赖实现的方式来更改字段。具有合理语义的唯一模式是构造一个对象,然后final更新该对象的字段的模式。在完成对对象字段的final所有更新之前,不应使该对象对其他线程可见,也不应读取final字段。final字段冻结发生在final设置该字段的构造函数的末尾,以及在final通过反射或其他特殊机制对字段进行每次修改之后立即冻结。

另一个问题是该规范允许对final字段进行积极的优化。在线程内,可以final使用final在构造函数中不进行的对字段的那些修改来重新排序对字段的读取。

示例17.5.3-1 进取的优化final领域
class A {
    final int x;
    A() { 
        x = 1; 
    } 

    int f() { 
        return d(this,this); 
    } 

    int d(A a1, A a2) { 
        int i = a1.x; 
        g(a1); 
        int j = a2.x; 
        return j - i; 
    }

    static void g(A a) { 
        // uses reflection to change a.x to 2 
    } 
}

在该d方法中,允许编译器自由地重新排列读取x和调用的顺序g。因此,new A().f()可以返回-101

实际上,在不违反上述法律情形的情况下确定可以进行积极优化的正确位置是一个未解决的问题,因此,除非-XX:+TrustFinalNonStaticFields已指定,否则HotSpot JVM不会final以与字段相同的方式来优化非静态static final字段。

当然,当您不将字段声明final为时,JIT不能假定它永远不会更改,但是,在没有线程同步原语的情况下,它可能会考虑在其优化的代码路径中发生的实际修改(包括反光的)。因此,它可能仍然积极优化访问,但只作为假设的读取和写入操作仍然在执行线程内的程序顺序发生。因此,当您从没有适当同步结构的其他线程查看优化时,只会注意到这些优化。


似乎很多人都在尝试利用此finals,但是,尽管有些人被证明具有更好的性能,但是节省一些人ns不值得破坏许多其他代码。例如,谢南多厄(Shenandoah)放弃某些旗帜的原因
尤金(Eugene)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.