Подтвердить что ты не робот

Возможно ли переупорядочение инициализации экземпляра и присвоение общей переменной?

Я читал эссе, в котором фактически говорится о двойной проверке блокировки, но я удивлен еще более принципиальным отказом в коде, представленном в качестве примеров. Там указано, что возможно, что инициализация экземпляров (т.е. Запись в переменные экземпляра, которые происходят до возвращения конструктора), может быть переупорядочена до того, как ссылка на экземпляр будет записана в общую переменную (статическое поле в следующий пример).

Верно ли, что при следующем определении класса 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 (и моделях памяти на других языках), на самом деле меня не удивляет, что это возможно, но интуиция очень сильно голосовала за то, что она невозможна (возможно, потому, что задействована инициализация объекта и объект инициализация кажется настолько священным в Java).

Можно ли "исправить" этот код (т.е. он никогда не будет печатать 0) без синхронизации в первом потоке?

4b9b3361

Ответ 1

Вызов foo = new Foo(); включает несколько операций, которые могут быть переупорядочены, если вы не введете правильную синхронизацию, чтобы предотвратить ее:

  • выделить память для нового объекта
  • записать значения полей по умолчанию (a = 0)
  • записать начальные значения полей (a = 1)
  • опубликовать ссылку на вновь созданный объект

Без правильной синхронизации шаги 3 и 4 могут быть переупорядочены (обратите внимание, что шаг 2 обязательно выполняется до шага 4), хотя это вряд ли произойдет с точкой доступа в архитектуре x86.

Чтобы предотвратить это, у вас есть несколько решений, например:

  • сделать a final
  • синхронизировать доступ к foo (с синхронизированным init и getter).

Не вдаваясь в тонкости JLS # 17, вы можете прочитать JLS # 12.4.1 о инициализации класса (выделение мое):

Тот факт, что код инициализации неограничен, позволяет создавать примеры, где значение переменной класса может наблюдаться, когда оно по-прежнему имеет начальное значение по умолчанию, прежде чем его выражение инициализации оценивается, но такое примеры на практике редки. ( Такие примеры могут быть также построены, например, для инициализации переменных.) В этих инициализаторах доступна полная мощность языка программирования Java; программисты должны проявлять определенную осторожность. Эта сила налагает дополнительное бремя на генераторы кода, но это бремя может возникнуть в любом случае, потому что язык программирования Java является параллельным.

Ответ 2

Переупорядочение инициализации экземпляра JIT-компилятором возможно даже под x86. Однако несколько сложнее написать код, который может вызвать такое переупорядочение. О том, как воспроизвести такое переупорядочение, см. Мой вопрос:

Есть ли какое-либо переупорядочение команд, выполняемое компилятором Hotspot JIT, которое можно воспроизвести?