在某些单元测试中,我遇到了一个奇怪的行为,即对最终静态场的反射。下面是一个说明我的问题的示例。
我有一个基本的Singleton类,其中包含一个Integer
public class BasicHolder {
private static BasicHolder instance = new BasicHolder();
public static BasicHolder getInstance() {
return instance;
}
private BasicHolder() {
}
private final static Integer VALUE = new Integer(0);
public Integer getVALUE() {
return VALUE;
}
}
我的测试用例是循环并通过反射将VALUE设置为迭代索引,然后断言VALUE正好等于迭代索引。
class TestStaticLimits {
private static final Integer NB_ITERATION = 10_000;
@Test
void testStaticLimit() {
for (Integer i = 0; i < NB_ITERATION; i++) {
setStaticFieldValue(BasicHolder.class, "VALUE", i);
Assertions.assertEquals(i, BasicHolder.getInstance().getVALUE(), "REFLECTION DID NOT WORK for iteration "+i);
System.out.println("iter " + i + " ok" );
}
}
private static void setStaticFieldValue(final Class obj, final String fieldName, final Object fieldValue) {
try {
final Field field = obj.getDeclaredField(fieldName);
field.setAccessible(true);
final Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, fieldValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Error while setting field [" + fieldName + "] on object " + obj + " Message " + e.getMessage(), e);
}
}
}
结果很令人惊讶,因为它不是恒定的, 我的测试在约1000次迭代中失败,但似乎永远不会一样。
有人遇到过这个问题吗?
答案 0 :(得分:10)
JLS提到在构造后修改最终字段是有问题的-请参阅 17.5. final Field Semantics
声明为final的字段只初始化一次,但在正常情况下不会更改。最终字段的详细语义与普通字段有所不同。尤其是,编译器具有极大的自由度,可以跨同步障碍移动对最终字段的读取,并可以调用任意或未知方法。相应地,允许编译器将最终字段的值保留在寄存器中缓存,并且在必须重新加载非最终字段的情况下,不从内存中重新加载它。
和17.5.3. Subsequent Modification of final Fields:
另一个问题是规范允许对最终字段进行积极优化。在线程内,可以使用在构造函数中不进行的对最终字段的修改来重新排序最终字段的读取。
除此之外,JavaDocs of Field.set还包含有关此内容的警告:
以这种方式设置最终字段仅在反序列化或重建具有空白最终字段的类的实例之前才有意义,然后才可以将它们用于程序的其他部分。在任何其他上下文中使用它可能会产生不可预测的影响,包括程序其他部分继续使用此字段的原始值的情况。
似乎我们在这里目睹的是JIT充分利用了语言规范所授予的重新排序和缓存的可能性。
答案 1 :(得分:4)
这是因为JIT优化。为了证明这一点,请使用以下VM
选项将其禁用:
-Djava.compiler=NONE
在这种情况下,所有10_000
迭代都将起作用。
或者,从编译中排除BasicHolder.getVALUE
方法:
-XX:CompileCommand=exclude,src/main/BasicHolder.getVALUE
实际上是在nth
迭代之后,正在编译热方法getVALUE
,并且正在积极优化static final Integer VALUE
(这实际上是及时的)。常量 1 )。从这一点开始,断言开始失败。
-XX:+PrintCompilation
的输出以及我的评论:
val 1 # System.out.println("val " + BasicHolder.getInstance().getVALUE());
val 2
val 3
...
922 315 3 src.main.BasicHolder::getInstance (4 bytes) # Method compiled
922 316 3 src.main.BasicHolder::getVALUE (4 bytes) # Method compiled
...
val 1563 # after compilation
val 1563
val 1563
val 1563
...