Подтвердить что ты не робот

Модель памяти Java: безопасно ли создавать циклический справочный граф полей конечных экземпляров, все они назначены в одном потоке?

Может ли кто-то, кто понимает модель памяти Java лучше меня, подтверждаю мое понимание того, что следующий код правильно синхронизирован?

class Foo {
    private final Bar bar;

    Foo() {
        this.bar = new Bar(this);
    }
}

class Bar {
    private final Foo foo;

    Bar(Foo foo) {
        this.foo = foo;
    }
}

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

Модель использования конечных полей является простой: установите конечные поля для объекта в этом объектном конструкторе; и не пишите ссылку на объект, который строится в месте, где другой поток может видеть его до завершения конструктора объекта. Если это будет выполнено, тогда, когда объект будет замечен другим потоком, этот поток всегда будет видеть правильно построенную версию конечных полей этого объекта. Он также будет видеть версии любого объекта или массива, на которые ссылаются те последние поля, которые, по крайней мере, соответствуют последним, как конечные поля. [Спецификация языка Java®: Java SE 7 Edition, раздел 17.5]

Другая ссылка:

Что означает, что объект должен быть правильно сконструирован? Это просто означает, что никакая ссылка на объект, который строится, не может "убежать" во время строительства. (Примеры см. В разделе "Методы безопасного строительства" ). Другими словами, не размещайте ссылку на объект, который строится где угодно, где другой поток может его увидеть; не присваивайте его статическому полю, не регистрируйте его как слушателя с любым другим объектом и так далее. Эти задачи должны выполняться после завершения конструктора, а не в конструкторе. [JSR 133 (модель памяти Java) Часто задаваемые вопросы: "Как работают последние поля в рамках нового JMM?"

4b9b3361

Ответ 1

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

Для вашего примера это довольно прямолинейно получить формально:

  • Для потока, который создает потоки, все наблюдаемые значения полей должны соответствовать порядку программы. Для этой внутрипоточной согласованности при построении Bar значение руки Foo соблюдается правильно и никогда null. (Это может показаться тривиальным, но модель памяти также регулирует "однопоточные" порядки памяти.)

  • Для любого потока, который получает экземпляр Foo, его ссылочное значение Bar может быть прочитано только через поле final. Это вводит порядок разнесения между чтением адреса объекта Foo и разыменованием поля объекта, указывающего на экземпляр Bar.

  • Если другой поток, таким образом, способен наблюдать экземпляр Foo вообще (формально, существует цепочка памяти), этот поток, как гарантируется, должен наблюдать это полностью Foo, что означает, что его Bar содержит полностью инициализированное значение.

Обратите внимание, что даже не имеет значения, что поле экземпляра Bar само является final, если экземпляр может быть прочитан только через Foo. Добавление модификатора не мешает и лучше документирует намерения, поэтому вы должны добавить его. Но, с учетом модели памяти, с вами все будет в порядке, даже без нее.

Обратите внимание, что кулинарная книга JSR-133, которую вы цитируете, описывает только реализацию модели памяти, а не сама модель памяти. Во многих случаях это слишком строго. В один прекрасный день OpenJDK больше не может согласовываться с этой реализацией и скорее внедрять менее строгую модель, которая по-прежнему соответствует формальным требованиям. Никогда не создавайте код против реализации, всегда указывайте код в соответствии со спецификацией!. Например, не полагайтесь на барьер памяти, который помещается после конструктора, как это делает HotSpot более или менее. Эти вещи не гарантируются и могут даже отличаться для разных аппаратных архитектур.

Правило с котировками, которое вы никогда не должны позволять исключению this escape-кода из конструктора, также слишком узкое представление о проблеме. Вы не должны позволять этому убегать в другой поток. Если вы, например, передадите его фактически отправленному методу, вы больше не сможете управлять тем, где экземпляр окажется в конечном итоге. Поэтому это очень плохая практика! Однако конструкторы не отправляются практически, и вы можете безопасно создавать круговые ссылки в том виде, как вы изображали. (Я предполагаю, что вы контролируете Bar и его будущие изменения. В общей базе кода вы должны документировать плотно, что конструктор Bar не должен позволять ссылочному выскользнуть.)

Ответ 2

Неизменяемые объекты (только с конечными полями) являются только "потоковыми" после того, как они правильно построены, что означает, что их конструктор завершен. (VM, вероятно, справляется с этим с помощью барьера памяти после конструктора таких объектов)

Давайте посмотрим, как сделать ваш пример неуверенным:

  • Если Bar-Constructor сохранит эту ссылку, где другой поток может ее увидеть, это будет небезопасно, потому что Bar еще не сконструирован.
  • Если Bar-Constructor будет хранить foo-reference, где другой поток может видеть это, это будет небезопасно, потому что foo isnt еще не сконструирован.
  • Если Bar-Constructor будет читать некоторые foo-поля, то (в зависимости от порядка инициализации внутри Foo-конструктора) эти поля всегда будут неинициализированы. Это не проблема потоковой безопасности, а просто эффект порядка инициализации. (Вызов виртуального метода внутри конструктора имеет те же проблемы)

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

Как уже писал Assylias: потому что в вашем примере конструкторы не хранили никаких ссылок на то, где другой поток мог их видеть, ваш пример "threadafe". Созданный Foo-Object можно безопасно предоставить другим потокам.