Инициализация не конечного поля

В настоящее время я читаю JSR-133 (модель памяти Java), и я не могу понять, почему f.y может быть не инициализирован (может видеть 0). Может кто-нибудь объяснить это мне?

class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample f;

    public FinalFieldExample() {
        x = 3;
        y = 4;
    }

    static void writer() {
        f = new FinalFieldExample();
    }

    static void reader() {
        if (f != null) {
            int i = f.x; // guaranteed to see 3
            int j = f.y; // could see 0
        }
    }
}

Ответ 1

Это называется эффектом "преждевременной публикации".

Положив это просто, JVM разрешено изменять порядок инструкций программы (по соображениям производительности), если такое переупорядочение не нарушает ограничений JMM.

Вы ожидаете, что код f = new FinalFieldExample(); будет запущен следующим образом:

1. создайте экземпляр FinalFieldExample
2. назначить 3 на x
3. назначьте 4 к y
4. назначить созданный объект переменной f

Но в предоставленном коде ничто не может остановить JVM из переупорядочения команд, поэтому он может запускать код следующим образом:

1. создайте экземпляр FinalFieldExample
2. назначить 3 на x
3. назначить исходный, не полностью инициализированный объект переменной f
4. назначьте 4 к y

Если переупорядочение происходит в среде с одним потоком, мы его даже не заметим. Это потому, что мы ожидаем, что объекты будут полностью созданы до того, как мы начнем работать с ними, и JVM уважает наши ожидания. Теперь, что может случиться, если несколько потоков одновременно запускают этот код? В следующем примере Thread1 выполняет метод writer() и Thread2 - метод reader():

Тема 1: создать экземпляр FinalFieldExample
Тема 1: назначить 3 на x
Тема 1: присвоить исходный, не полностью инициализированный объект переменной f
Тема 2: чтение f, это не пусто
Тема 2: чтение f.x, это 3
Тема 2: чтение f.y, все равно 0
Тема 1: назначить 4 на y

Определенно не хорошо. Чтобы не допустить JVM, нам нужно предоставить дополнительную информацию о программе. В этом конкретном примере существуют некоторые способы исправления последовательности:

  • объявить y как переменную final. Это вызовет эффект freeze". Короче говоря, конечные переменные всегда будут инициализироваться в тот момент, когда вы обращаетесь к ним, если ссылка на объект не была пропущена во время строительства.
  • объявить f как переменную volatile. Это создаст " порядок синхронизации и устранит проблему. Короче говоря, инструкции не могут быть переупорядочены ниже волатильной записи и выше изменчивого чтения. Присвоение переменной f - это volatile write, что означает, что инструкции new FinalFieldExample() не могут быть переупорядочены и выполнены после назначения. Чтение из переменной f является изменчивым чтением, поэтому чтение f.x невозможно выполнить перед ним. Комбинация v-write и v-read называется порядком синхронизации и обеспечивает желаемую согласованность памяти.

Здесь - хороший блог, который может ответить на все ваши вопросы о JMM.

Ответ 2

JVM может изменять порядок чтения и записи памяти, поэтому ссылка на f может быть записана в основную память до значения f.y. Если другой поток читает f.y между этими двумя записями, он будет читать 0. Однако, если вы создаете барьер памяти, написав поле final или volatile, читайте и записывайте после того, как барьер не может быть переупорядочен по отношению к чтению и записи перед барьером. Поэтому он гарантирует, что как f, так и f.y будут записаны до того, как другой поток прочитает f.

Я задал аналогичный вопрос здесь. Ответы идут намного подробнее.

Ответ 3

Модель памяти Java позволяет потоку создавать FinalFieldExample, инициализировать окончательный x и сохранять ссылку на экземпляр FinalFieldExample до f перед инициализацией нефинального y.