Ответ 1
Процитированная вами статья является концептуально правильной. Это несколько неточно в его терминологии и использовании, как и ваш вопрос, и это приводит к потенциальной недопониманию и недоразумениям. Может показаться, что я занимаюсь терминологией здесь, но модель памяти Java очень тонкая, и если терминология не является точной, то одно понимание будет страдать.
Я вычитаю баллы из вашего вопроса (и из комментариев) и даю ответы на них.
Назначение значения, возвращаемого конструктором, может быть переупорядочено относительно инструкций внутри конструктора.
Почти да... это не инструкции, а операции с памятью (чтение и запись), которые могут быть переупорядочены. Поток мог выполнять две команды записи в определенном порядке, но приход данных в память и, следовательно, видимость этих записей в другие потоки может происходить в другом порядке.
Я думаю, что это гарантировало, что с точки зрения потока, выполняющего
MyInt a = new MyInt(42)
, присваиваниеx
имеет взаимосвязь между событиями с назначениемa
.
Опять же, почти. Верно, что в программном порядке назначение x
происходит до назначения на a
. Однако before-before является глобальным свойством, которое применяется ко всем потокам, поэтому не имеет смысла говорить о случившемся раньше, чем в отношении конкретного потока.
Но оба эти значения могут быть кэшированы в регистрах, и они не могут быть сброшены в основную память в том же порядке, в котором они были изначально написаны. Без барьера памяти другой поток может поэтому прочитать значение a до того, как будет записано значение x.
Опять же, почти. Значения можно кэшировать в регистрах, но части аппаратного обеспечения памяти, такие как кэш-память или буфер записи, также могут привести к переупорядочению. Аппаратное обеспечение может использовать различные механизмы для изменения порядка, например, для очистки кеша или барьеров памяти (которые обычно не вызывают промывки, а просто предотвращают определенные переупорядочивания). Трудность думать об этом с точки зрения аппаратного обеспечения заключается в том, что реальные системы довольно сложны и имеют разные формы поведения. Например, у большинства ЦП есть несколько различных вариантов барьеров памяти. Если вы хотите рассуждать о JMM, вам следует подумать с точки зрения элементов модели: операции с памятью и синхронизация, которые ограничивают переупорядочивание, устанавливая отношения "когда-либо".
Итак, чтобы пересмотреть этот пример с точки зрения JMM, мы увидим запись в поле x
и запись в поле a
в программном порядке. В этой программе нет ничего, что ограничивает переупорядочивание, т.е. Никакой синхронизации, никаких операций с летуацами, не пишет в конечные поля. Не происходит - до того, как отношения между ними будут записаны, и поэтому их можно будет переупорядочить.
Существует несколько способов предотвращения этих переупорядочений.
Один из способов - сделать x
final. Это работает, потому что 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
, поскольку он имеет окончательный массив полей, содержащий его символы. Да, это проблема, но если вы посмотрите на конструкторы String.java, все они очень осторожны, чтобы назначить конечное поле в самом конце конструктора. Это обеспечивает правильную видимость содержимого массива.
И да, это тонко.:-) Но проблемы на самом деле возникают, если вы пытаетесь быть умными, например, избегать использования синхронизации или изменчивых переменных. Большую часть времени это не стоит. Если вы придерживаетесь практики "безопасной публикации", в том числе не утечка this
во время вызова конструктора и сохранение ссылок на построенные объекты с использованием синхронизации (например, мой пример OtherObj
выше), все будет работать точно так, как вы ожидаете от них.
Литература:
- Goetz, Java Concurrency На практике, глава 3, Совместное использование объектов. Это включает в себя обсуждение памяти и безопасной публикации.
- Manson/Goetz, Часто задаваемые вопросы о модели памяти Java. http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html. Несколько старый, но есть хорошие примеры.
- Shipilev, Java Memory Model Pragmatics. http://shipilev.net/blog/2014/jmm-pragmatics/. Слайд-презентация и расшифровка беседы, предоставленной одним из гуру производительности Oracle. Больше, чем вы хотели узнать о JMM, с некоторыми указателями на возможные изменения JMM в будущих версиях Java.
- Исходный код OpenJDK 8 String.java. http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/lang/String.java