Возможно ли переупорядочение инициализации экземпляра и присвоения общей переменной?
Я читал эссе, в котором на самом деле говорится о двойной проверке блокировки, но меня удивляет еще более базовая ошибка в коде, представленном в качестве примеров. Там указано, что возможно, что инициализация экземпляров (то есть, запись в переменные экземпляра, которые происходят до возврата конструктора) может быть переупорядочена после записи ссылки на экземпляр в общую переменную (статическое поле в следующий пример).
Правда ли, что при следующем определении класса Foo
с выполнением одного потока Foo.initFoo();
и другой поток, выполняющий System.out.println(Foo.foo.a);
вторая нить может распечатать 0
(вместо 1
или бросать NullPointerException
)?
class Foo {
public int a = 1;
public static Foo foo;
public static void initFoo() {
foo = new Foo();
}
public static void thread1() {
initFoo(); // Executed on one thread.
}
public static void thread2() {
System.out.println(foo.a); // Executed on a different thread
}
}
Из того, что я знаю о модели памяти Java (и о моделях памяти в других языках), на самом деле меня не удивляет, что это возможно, но интуиция очень сильно голосует за то, что это невозможно (возможно, потому что инициализация объекта задействована, а инициализация объекта кажется такой священное на Яве).
Можно ли "исправить" этот код (то есть, что он никогда не будет печатать 0
) без синхронизации в первом потоке?
2 ответа
Вызов foo = new Foo();
включает в себя несколько операций, которые могут быть переупорядочены, если вы не введете правильную синхронизацию, чтобы предотвратить это:
- выделить память для нового объекта
- напишите значения полей по умолчанию (
a = 0
) - напишите начальные значения полей (
a = 1
) - опубликовать ссылку на вновь созданный объект
Без надлежащей синхронизации шаги 3 и 4 могут быть переупорядочены (обратите внимание, что шаг 2 обязательно происходит перед шагом 4), хотя это вряд ли произойдет с горячей точкой в архитектуре x86.
Чтобы предотвратить это, у вас есть несколько решений, например:
- делать
a
окончательный - синхронизировать доступ к
foo
(с синхронизированнымinit
И добытчик).
Не вдаваясь в тонкости JLS #17, вы можете прочитать JLS #12.4.1 об инициализации класса (выделено мое):
Тот факт, что код инициализации неограничен, позволяет создавать примеры, в которых значение переменной класса можно наблюдать, когда оно все еще имеет свое начальное значение по умолчанию, до того, как будет оценено его инициализирующее выражение, но такие примеры на практике редки. (Такие примеры также могут быть созданы для инициализации переменной экземпляра.) В этих инициализаторах доступны все возможности языка программирования Java; программисты должны проявлять осторожность. Эта сила накладывает дополнительную нагрузку на генераторы кода, но эта нагрузка возникнет в любом случае, потому что язык программирования Java является параллельным.
Изменение порядка инициализации экземпляра компилятором JIT возможно даже под x86. Однако несколько сложно написать код, который может инициировать такое переупорядочение. О том, как воспроизвести такое переупорядочение, см. Мой вопрос: