Параллельный доступ к общественному полю. Почему можно наблюдать противоречивое состояние?

Я читаю B. Goetz Java Concurrency На практике, и теперь я в section 3.5 о безопасной публикации. Он заявил:

// Unsafe publication
public Holder holder;
public void initialize() {
    holder = new Holder(42);
}

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

Я не понимаю, почему можно наблюдать частично построенный подобъект. Предположим, что конструктор Holder(int) не позволяет this уйти. Таким образом, построенная ссылка может наблюдаться только вызывающим. Теперь, как JLS 17.7, говорится:

Писания и чтения ссылок всегда являются атомарными, независимо от того, независимо от того, реализованы ли они как 32-битные или 64-битные значения.

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

Где я был неправ?

Ответ 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 возвращает ссылку на выделенный объект и устанавливает 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.

Ответ 2

Конструкция Holder состоит примерно из трех частей:

  • выделить память
  • запустить инициализаторы
  • присвоить поле значения Holder

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

  • выделить память
  • присвоить поле значения Holder
  • запустить инициализаторы

Таким образом, возможно, что частично сконструированный объект уже назначен для поля. С одной точки зрения это не представляет никакой проблемы. Но с многопоточной точки зрения это приводит к очевидным проблемам.

Ответ 3

Теперь, как указано в JLS 17.7, "Запись и чтение ссылок всегда являются атомарными..."

Это может означать меньше, чем вы думаете. Все это означает, что значение, присвоенное ссылочной переменной, никогда не будет разорвано.

Если какой-либо поток обновляет неизменяемую ссылочную переменную, которая первоначально ссылалась на объект A, изменяя его, чтобы вместо этого ссылаться на объект B, тогда другие потоки либо видят ссылку A, либо ссылку B, когда они исследуют переменную. Ни один нить никогда не увидит неверную ссылку, состоящую из некоторых битов от старого значения и некоторых битов от нового значения.

В частности, требование, чтобы все ссылки на чтение и запись были "атомарными", не означает, что все ссылки на чтение и запись имеют семантику volatile. Они этого не делают; Если один поток обновляет неизменяемую ссылочную переменную, указывающую на вновь созданный объект, тогда другой поток может получить новую ссылку, когда она исследует переменную, но видит сам объект в частично инициализированном или неинициализированном состоянии.