Когда я возился с модульными тестами для высококонкурентного синглтон-класса, я наткнулся на следующее странное поведение (проверено на 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);
}
Последние 2 строки метода main () расходятся во мнении о значении INSTANCE - я предполагаю, что JIT полностью избавился от метода, так как поле является статическим final. Удаление последнего ключевого слова приводит к тому, что код выводит правильные значения.
Оставляя в стороне вашу симпатию (или ее отсутствие) к одиночкам и на минуту забываем, что использование такого отражения вызывает проблемы - верно ли мое предположение в том, что виноваты оптимизации JIT? Если это так - ограничены ли они только статическими конечными полями?
java
reflection
java-8
jit
Kelm
источник
источник
static final
полем. Кроме того, не имеет значения, сломался ли этот взлом отражения из-за JIT или параллелизма.Ответы:
Если взять ваш вопрос буквально: « … верно ли мое предположение, что виноваты оптимизации JIT? ”, Ответ - да, очень вероятно, что JIT-оптимизации ответственны за это поведение в этом конкретном примере.
Но так как изменение
static final
полей полностью не соответствует спецификации, есть и другие вещи, которые могут нарушить его аналогичным образом. Например, JMM не имеет определения видимости таких изменений в памяти, следовательно, совершенно не определено, замечают ли другие потоки такие изменения или нет. Они даже не обязаны замечать это последовательно, то есть они могут использовать новое значение, после чего снова использовать старое значение, даже при наличии примитивов синхронизации.Тем не менее, JMM и оптимизатор в любом случае здесь трудно разделить.
Ваш вопрос « … ограничены ли они только статическими конечными полями? ”Гораздо труднее ответить, так как оптимизация, конечно, не ограничивается
static final
полями, но поведение, например, нестатическихfinal
полей, не одинаково и имеет различия между теорией и практикой.Для нестатических
final
полей модификации через Reflection разрешены при определенных обстоятельствах. На это указывает тот факт, что этогоsetAccessible(true)
достаточно, чтобы сделать возможной такую модификацию, не взламываяField
экземпляр, чтобы изменить внутреннееmodifiers
поле.В спецификации сказано:
На практике определение правильных мест, где возможна агрессивная оптимизация без нарушения правовых сценариев, описанных выше, является открытой проблемой , поэтому, если
-XX:+TrustFinalNonStaticFields
это не указано, JVM HotSpot не будет оптимизировать нестатическиеfinal
поля так же, какstatic final
поля.Конечно, когда вы не объявляете поле как
final
, JIT не может предполагать, что оно никогда не изменится, хотя, в отсутствие примитивов синхронизации потоков, он может учитывать фактические изменения, происходящие в пути кода, который он оптимизирует (включая отражающие). Таким образом, он все еще может активно оптимизировать доступ, но только в том случае, если чтение и запись по-прежнему происходят в программном порядке в потоке выполнения. Таким образом, вы заметите оптимизации только при взгляде на него из другого потока без правильных конструкций синхронизации.источник
final
, но, хотя некоторые из них оказались более производительными, экономия некоторыхns
не стоит ломать много другого кода. Например,