Ответ 1
Таким образом, построенная ссылка может наблюдаться только вызывающим.
То, где ваша логика ломается, хотя кажется, что это вполне разумная вещь.
Прежде всего: атомарность, о которой говорится в 17.7, говорит только о том, что когда вы читаете ссылку, вы увидите либо все предыдущее значение (начиная со значения по умолчанию null
), либо все последующее значение. Вы никогда не получите ссылку с некоторыми битами, соответствующими значению 1, и некоторым битам, соответствующим значению 2, что по существу сделает его ссылкой в случайное место в куче JVM - что было бы ужасно! В основном они говорят: "сама ссылка будет либо нулевой, либо указывает на допустимое место в памяти". Но что в этой памяти, где вещи могут стать странными.
Настройка простого примера
Я предполагаю, что этот простой держатель:
public class Holder {
int value; // NOT final!
public Holder(int value) { this.value = value; }
}
Учитывая, что происходит, когда вы делаете holder = new Holder(42)
?
- JVM выделяет некоторое пространство для нового объекта Holder со значениями по умолчанию для всех его полей (т.е.
value = 0
) - JVM вызывает конструктор Holder
- JVM устанавливает
<new instance>.value
в поступающее значение (42). - конструктор завершает
- JVM устанавливает
- JVM возвращает ссылку на выделенный объект и устанавливает
Holder.holder
в эту новую ссылку
Переупорядочение делает жизнь тяжелой (но она также делает программы быстрыми!)
Проблема в том, что другой поток может просматривать эти события в любом порядке, так как между ними нет точек синхронизации. Это потому, что конструкторы не имеют какой-либо специальной синхронизации или происходят - перед семантикой (это небольшая ложь, но больше об этом позже). Вы можете увидеть полный список "синхронизированных с" действиями в JLS 17.4.4; Заметьте, что там ничего нет о конструкторах.
Таким образом, другой поток может видеть, что эти действия упорядочены как (1, 3, 2). Это означает, что если какое-то другое событие упорядочено между событиями 1 и 3 - например, если кто-то читает Holder.holder.value
в локальный var, то они будут видеть этот недавно выделенный объект, но с его значениями до запуска конструктора: вы 'd см. Holder.holder.value == 0
. Это называется частично построенным объектом, и это может быть довольно запутанным.
Если конструктор имел несколько шагов (установка нескольких полей или установка, а затем изменение поля), вы можете увидеть любое упорядочение этих шагов. Практически все ставки отключены. Хлоп!
Конструкторы и final
поля
Я упомянул выше, что я лгал, когда утверждал, что у конструкторов нет специальной семантики синхронизации. Предполагая, что вы не просачиваетесь this
, есть одно исключение: любые поля final
гарантированно будут отображаться как они были в конце конструктора (см. JLS 17.5).
Вы можете думать об этом, поскольку между этапами 2 и 3 существует какая-то точка синхронизации, но она применима только к полям final
.
- Это не относится к незавершенным полям
- Он не применяется транзитно к другим точкам синхронизации.
- Однако он распространяется на любое состояние, доступ к которому осуществляется через поля
final
. Итак, если у вас естьfinal List<String>
, и ваш конструктор инициализирует его, а затем добавляет некоторые значения, то все потоки гарантированно будут видеть этот список, по крайней мере, с состоянием, которое у него было в конце конструктора, включая теadd
звонки. (Если вы измените список после конструктора, без синхронизации, все ставки снова отключится.)
Вот почему в моем примере выше было важно, чтобы value
не был окончательным. Если бы это было так, вы бы не смогли увидеть Holder.holder.value == 0
.