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

Нужно ли синхронизировать мутацию небезобезопасной коллекции в конструкторе?

Если я решил использовать сборку без потоковой передачи и синхронизировать ее доступ, нужно ли мне синхронизировать любую мутацию в конструкторе? Например, в следующем коде я понимаю, что ссылка на список будет видна для всех постсоборных потоков, поскольку она является окончательной. Но я не знаю, является ли это безопасной публикацией, потому что добавление в конструктор не синхронизируется и добавляет ссылку в массив ArrayList elementData, что не является окончательным.

private final List<Object> list;

public ListInConstructor()
{
    list = new ArrayList<>();
    // synchronize here?
    list.add(new Object());
}

public void mutate()
{
    synchronized (list)
    {
        if (list.checkSomething())
        {
            list.mutateSomething();
        }
    }
}
4b9b3361

Ответ 1

Хорошо, так это то, что JLS §17.5.1 должно сказать по этой теме.

Прежде всего:

Пусть o - объект, а c - конструктор для o, в котором конечный поле f записано. Выполняется действие замораживания в последнем поле f of o когда c выходит, либо нормально, либо резко.

Итак, мы знаем, что в нашем коде:

public ListInConstructor() {
    list = new ArrayList<>();
    list.add(new Object());
} // the freeze action happens here!

Итак, теперь интересная часть:

Учитывая запись w, замораживание f, действие a (то есть не считанное конечное поле), прочитанное r1 конечного поля, замороженного f, и прочитанное r2 такой, что hb (w, f), hb (f, a), mc (a, r1) и разложения (r1, r2), то при определении того, какие значения можно видеть через г2, рассмотрим hb (w, r2).

Итак, сделайте это за один раз:

У нас есть hb (w, f), что означает, что мы записываем в конечное поле перед тем, как покинуть конструктор.

r1 - это чтение конечного поля и разделов (r1, r2). Это означает, что r1 считывает конечное поле, а r2 затем считывает некоторое значение этого конечного поля.

У нас также есть действие (чтение или запись, но не чтение конечного поля), которое имеет hb (f, a) и mc (a, r1). Это означает, что действие происходит после конструктора, но может быть видно после чтения r1.

И, следовательно, в нем говорится, что "мы рассматриваем hb (w, r2)", что означает, что запись должна произойти до чтения для значения конечного поля, которое было прочитано с r1.

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

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

Ответ 2

Обновление: Спецификация языка Java гласит, что заморозить делает изменения видимыми должен быть в конце конструктора, что означает, что ваш код правильно синхронизированы, см. ответы John Vint и Voo.

Однако вы также можете это сделать, что определенно работает:

public ListInConstructor()
{
    List<Object> tmp = new ArrayList<>();
    tmp.add(new Object());
    this.list = tmp;
}

Здесь мы мутируем объект списка до, назначая его в поле final, и поэтому назначение будет гарантировать, что любые изменения, внесенные в список, также будут видны.

17.5. окончательная полевая семантика

Модель использования для окончательных полей проста: установите конечные поля для объекта в этом объектном конструкторе; и не пишите ссылка на объект, который строится в месте, где поток может видеть его до завершения конструктора объекта. Если это, тогда, когда объект просматривается другим потоком, это нить всегда будет видеть правильно построенную версию этого конечные поля объекта. Он также увидит версии любого объекта или массив, на который ссылаются те конечные поля, которые, по крайней мере, являются последними как конечные поля.

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

Ответ 3

В соответствии с JLS

Модель использования конечных полей является простой: установите конечные поля для объекта в этом объектном конструкторе; и не пишите ссылку на объект, который строится в месте, где другой поток может видеть его до завершения конструктора объекта.

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

edit: Основываясь на комментарии Voo, я сделаю редактирование с включением окончательного замораживания поля.

Поэтому, прочитав больше в 17.5.1, эта запись

Для записи w, замораживания f, действия a (не считанного конечного поля), прочитанного r1 конечного поля, замороженного f, и чтения r2, такого, что hb (w, f), hb (f, a), mc (a, r1) и разложения (r1, r2),

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

Ответ 4

Поскольку объект не является неотъемлемо неизменным, вы должны спокойно опубликовать объект. Пока вы это делаете, нет необходимости делать мутации в конструкторе в блоке synchronized.

Объекты могут быть "безопасно опубликованы" несколькими способами. Примером является передача их в другой поток через правильно синхронизированную очередь. Подробнее см. Java Concurrency в разделе Практика 3.5.3 "Безопасные идиомы публикации" и 3.5.4 "Эффективно неизменяемые объекты"