使用反射打破JIT优化

8

我在处理高并发单例类的单元测试时,遇到了以下奇怪的行为(在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);
}

主方法的最后两行对INSTANCE的值持不同意见-我猜测JIT已完全摆脱了该方法,因为该字段是静态final的。 移除final关键字使代码输出正确的值。
抛开你对单例模式的同情(或缺乏同情),暂且忘记使用反射可能会遇到麻烦 - 我的假设是否正确,即JIT优化应该受到责备? 如果是这样-这些局限于静态final字段吗?

1
单例是一种只能存在一个实例的类。因此,你没有一个单例,你只有一个带有static final字段的类。除此之外,这个反射hack是否由于JIT或并发而中断并不重要。 - Holger
@Holger 这个 hack 只是在单元测试中完成的,旨在模拟单例以供使用它的类的多个测试用例。我不明白并发如何引起这个问题(上面的代码中没有并发),我真的很想知道发生了什么。 - Kelm
1
你在问题中提到“高并发单例类”,我则说“这并不重要”。所以,如果你特定的示例代码由于JIT而出现故障,并且你找到了解决方法,那么当真正的代码从由于JIT而引起的故障变为由于并发而引起的故障时,你又获得了什么呢? - Holger
@Holger 好的,我的措辞有点过于强烈了,对此我很抱歉。我的意思是这样的——如果我们不理解为什么某些事情会出现严重问题,那么我们将来就容易再次受到同样的伤害,所以我宁愿知道原因,而不是假设“它只是发生了”。无论如何,感谢您抽出时间来回答! - Kelm
1个回答

6
采用字面意思理解你的问题,“…我的假设是JIT优化有问题,这个假设正确吗?”,答案是肯定的,很可能在这个具体的例子中JIT优化是导致这种行为的罪魁祸首。
但是,由于更改“static final”字段完全不符合规范,还有其他可能会破坏它的事情。例如,JMM没有定义这些更改的内存可见性,因此,完全没有指定其他线程何时或是否注意到这些更改。它们甚至不需要一致地注意到它,即使存在同步原语,也可能使用新值,然后再次使用旧值。
虽然,在这里很难将JMM和优化器分开。
你的问题“…这些限制仅适用于静态final字段吗?”更难回答,因为优化当然不仅限于“static final”字段,但是例如非静态“final”字段的行为与理论和实践之间也有差异。
对于非静态的final字段,在某些情况下允许通过反射进行修改。这表明,仅通过setAccessible(true)就足以使此类修改成为可能,而无需入侵Field实例以更改内部modifiers字段。 规范指出:

17.5.3. Subsequent Modification of final Fields

In some cases, such as deserialization, the system will need to change the final fields of an object after construction. final fields can be changed via reflection and other implementation-dependent means. The only pattern in which this has reasonable semantics is one in which an object is constructed and then the final fields of the object are updated. The object should not be made visible to other threads, nor should the final fields be read, until all updates to the final fields of the object are complete. Freezes of a final field occur both at the end of the constructor in which the final field is set, and immediately after each modification of a final field via reflection or other special mechanism.

Another problem is that the specification allows aggressive optimization of final fields. Within a thread, it is permissible to reorder reads of a final field with those modifications of a final field that do not take place in the constructor.

Example 17.5.3-1. Aggressive Optimization of final Fields
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 
    } 
}

In the d method, the compiler is allowed to reorder the reads of x and the call to g freely. Thus, new A().f() could return -1, 0, or 1.

实际上,确定可以进行激进优化而不会破坏上述法律情景的正确位置是一个未解决的问题,因此,除非指定了-XX:+TrustFinalNonStaticFields,否则HotSpot JVM将不会像对待static final字段一样优化非静态final字段。

当然,如果您没有将字段声明为final,JIT无法假定它永远不会更改,尽管在缺少线程同步原语的情况下,它可能会考虑代码路径中发生的实际修改(包括反射修改)。因此,它仍然可以积极优化访问,但只有在执行线程内按程序顺序读取和写入时才会出现。因此,只有在使用不正确的同步结构从不同线程查看时才会注意到优化。


似乎有很多人试图利用这些“final”,但是,尽管有些已经被证明性能更好,但节省一些“ns”并不值得破坏大量其他代码。这就是为什么Shenandoah在某些标志上退缩的原因 - Eugene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接