Конструкторы и переупорядочивание инструкций

Я только что наткнулся статьи это делает заявление, которое я никогда не слышал раньше и не могу найти нигде больше. Утверждение заключается в том, что с точки зрения другого потока присвоение значения, возвращаемого конструктором, может быть переупорядочено относительно инструкций внутри конструктора. Другими словами, утверждение заключается в том, что в приведенном ниже коде другой поток может прочитать ненулевое значение a, в которой значение x не было набор.

class MyInt {
   private int x;

   public MyInt(int value) {
      x = value;
   }

   public int getValue() {
      return x;
   }
}

MyInt a = new MyInt(42);

это правда?

Edit:

Я думаю, что это гарантировано с точки зрения выполнения потока MyInt a = new MyInt(42), назначение x имеет отношение случается-перед назначением a. Но оба эти значения могут кэшироваться в регистрах, и они не могут быть сброшены в основную память в том же порядке, в котором они были первоначально записаны. Без барьера памяти другой поток мог бы поэтому прочитать значение a перед значением x понаписали. Правильно?

так axtavtответ и комментарии, которые следуют, являются ли эти оценки безопасности потоков правильными?

// thread-safe
class Foo() {
   final int[] x;

   public Foo() {
      int[] tmp = new int[1];
      tmp[0] = 42;
      x = tmp; // memory barrier here
   }
}

// not thread-safe
class Bar() {
   final int[] x = new int[1]; // memory barrier here

   public Bar() {
      x[0] = 42; // assignment may not be seen by other threads
   }
}

если это правильно... Вау,это очень тонко.

3 ответов


статья, которую вы процитировали, концептуально верна. Он несколько неточен в своей терминологии и использовании, как и ваш вопрос, и это приводит к потенциальному недопониманию и недопониманию. Может показаться, что я здесь повторяю терминологию, но модель памяти Java очень тонкая, и если терминология не точна, то понимание пострадает.

я буду отрывать пункты из вашего вопроса (и из комментариев) и предоставлять ответы на них.

присвоение значения, возвращаемого конструктором, может быть переупорядочено относительно инструкций внутри конструктора.

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

я думаю, что это гарантировано с точки зрения выполнения потока MyInt a = new MyInt(42) назначение x имеет отношение happens-before с назначением a.

опять же, почти. Это правда, что в программы это задание x происходит до назначения a. Однако,происходит-перед является глобальным свойством, которое применяется ко всем потокам, поэтому оно не имеет смысл говорить о том, что происходит-раньше по отношению к определенной теме.

но оба эти значения могут быть кэшируются в регистрах, и они не могут быть сброшены в основную память, в том же порядке, в котором они были написаны. Поэтому без барьера памяти другой поток может прочитать значение a до того, как будет записано значение x.

еще раз, почти. Значения можно кэшировать в регистрах, но части оборудования памяти, такие как кэш буферы памяти или записи также могут привести к переупорядочению. Оборудование может использовать различные механизмы для изменения порядка, такие как промывка кэша или барьеры памяти (которые обычно не вызывают промывку, а просто предотвращают определенные переупорядочения). Однако трудность с точки зрения аппаратного обеспечения заключается в том, что реальные системы довольно сложны и имеют различное поведение. Например, большинство процессоров имеют несколько различных типов барьеров памяти. Если вы хотите рассуждать о JMM, вы следует мыслить в терминах элементов модели: операций памяти и синхронизации, которые ограничивают переупорядочения, устанавливая отношения "происходит до".

Итак, чтобы вернуться к этому примеру с точки зрения JMM, мы видим запись в поле x и пишем в поле a в целях программы. В этой программе нет ничего, что ограничивает переупорядочение, т. е. нет синхронизации, нет операций над летучими веществами, нет записи в конечные поля. Не бывает-до отношений между этими записями и, следовательно, они могут быть переупорядочены.

есть несколько способов предотвратить эти переупорядочения.

один из способов-сделать x финал. Это работает, потому что JMM говорит, что записывает в конечные поля до того, как конструктор возвращает происходят-перед операции, которые происходят после возвращения конструктор. С a записывается после возвращения конструктора, инициализация конечного поля x случается-перед записью в a, и никакого порядка не допускается.

другой способ - использовать синхронизацию. Предположим, что MyInt экземпляр был использован в другом классе, как это:

class OtherObj {
    MyInt a;
    synchronized void set() {
        a = new MyInt(42);
    }
    synchronized int get() {
        return (a != null) ? a.getValue() : -1;
    }
}

разблокировка в конце set() вызов происходит после записи в x и a поля. Если другой поток вызывает get(), он принимает блокировку в начале вызова. Это устанавливает связь "происходит до" между освобождением блокировки в конце set() и замок приобретение в начале get(). Это означает, что пишет x и a не может быть переупорядочен после начала get() звонок. Таким образом, поток чтения увидит допустимые значения для обоих a и x и никогда не может найти ненулевое a и неинициализированный x.

конечно, если поток чтения вызывает get() ранее он может видеть a как null, но здесь нет проблемы с моделью памяти.

код Foo и Bar примеры интересны, и Ваша оценка по существу правильна. Запись в элементы массива, которые происходят до назначения в конечное поле массива, не может быть переупорядочена после. Записи в элементы массива, которые происходят после присвоения окончательному полю массива, могут быть переупорядочены относительно других операций памяти, которые происходят позже, поэтому другие потоки действительно могут видеть устаревшие значения.

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

и да, это тонкий. :- ) Но проблемы возникают только в том случае, если вы пытаетесь быть умным, например, пытаетесь избежать использования синхронизации или изменчивых переменных. В большинстве случаев это не так. стоит того. Если вы придерживаетесь практики "безопасной публикации", в том числе не утечка this во время вызова конструктора, так и хранение ссылок на объекты с помощью синхронизации (например,OtherObj пример выше), все будет работать именно так, как вы ожидаете их.

ссылки:

  • Goetz,Параллелизм Java На Практике Глава 3 Совместное Использование Объектов. Это включает обсуждение visbility памяти и сейфа публикация.
  • Мэнсон/Гетц, Java Memory Model FAQ. http://www.cs.umd.edu / ~pugh/java/memoryModel/jsr-133-faq.html . Несколько староват, но имеет несколько хороших примеров.
  • Шипилев, Прагматика Модели Памяти Java. http://shipilev.net/blog/2014/jmm-pragmatics/ . Слайд-презентация и стенограмма выступления одного из гуру производительности Oracle. Больше, чем вы когда-либо хотели знать о JMM, с некоторыми указателями на потенциальные изменения JMM в будущих версиях Java.
  • Строка OpenJDK 8.исходный код Java. http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/lang/String.java

В смысле модели памяти Java - да. В не значит, что вы будете наблюдать его на практике.

посмотрите на это под следующим углом: оптимизация, которая может привести к видимому переупорядочению, может произойти не только в компиляторе, но и в ЦП. Но процессор ничего не знает об объектах и их конструкторах, для процессора это всего лишь пара назначений, которые можно переупорядочить, если это позволяет модель памяти процессора.

конечно, компилятор и JVM может проинструктировать CPU не переупорядочивать эти назначения, помещая барьеры памяти в сгенерированном коде, но это для всех объектов разрушит производительность процессоров, которые могут сильно полагаться на такую агрессивную оптимизацию. Вот почему модель памяти Java не предоставляет никаких специальных гарантий для этого случая.

Это приводит, например, к известному недостатку в двойная проверка блокировки одноэлементной реализации под моделью памяти Java.


другими словами, утверждение заключается в том, что в приведенном ниже коде другой поток может считывать ненулевое значение a, в котором значение x не было установлено.

короткий ответ-да.

длинный ответ : точка, которая лежит в основе другого потока, считывающего ненулевое a стоимостью x это не было установлено-это не строго переупорядочивание инструкций, но процессор кэширует значения в своих регистрах (и кэш L1 ) скорее чем чтение этих значений из основной памяти. Это может косвенно подразумевать изменение порядка, но в этом нет необходимости.

хотя кэширование значений в регистрах ЦП помогает ускорить обработку, это вводит проблему видимости значений между различными потоками, работающими на разных процессорах. Если значения всегда считывались из основной области программы, все потоки будут последовательно видеть одно и то же значение ( потому что там одну копию этого значения ) . В вашем примере кода , если значение поля x кэшируется в регистр CPU1, к которому обращается thread-1 , а другой поток Thread-2 , работающий на CPU-2, теперь считывает это значение из основной памяти и обновляет его, значение этого как кэшированное в CPU-1 ( обработанное потоком-1 ) теперь недействительно с точки зрения программы, но сама спецификация Java позволяет виртуальным машинам рассматривать это как допустимый сценарий.