Должен ли я отмечать атрибуты объектов как изменчивые, если я их инициализирую в @PostConstruct в Spring Framework?

Предположим, что я выполняю некоторую инициализацию в Spring singleton bean @PostConstruct (упрощенный код):

@Service
class SomeService {
  public Data someData; // not final, not volatile

  public SomeService() { }

  @PostConstruct
  public void init() {
     someData = new Data(....);
  }
}

Должен ли я беспокоиться о видимости someData для других beans и отмечать его volatile?

(предположим, что я не могу инициализировать его в конструкторе)

И второй сценарий: что, если я перезаписать значение в @PostConstruct (после, например, явной инициализации или инициализации в конструкторе), поэтому запись в @PostConstruct не будет первой записи к этому атрибуту?

Ответы

Ответ 1

Среда Spring не привязана к языку программирования Java, это всего лишь среда. Поэтому, как правило, необходимо пометить конечное поле non-, к которому обращаются разные потоки, чтобы оно было volatile. В конце концов, bean-компонент Spring является не чем иным, как объектом Java, и применяются все языковые правила.

final поля получают специальную обработку на языке программирования Java. Александр Шипилев, специалист по производительности Oracle, написал отличную статью по этому вопросу. Короче говоря, когда конструктор инициализирует final поле, сборка для установки значения поля добавляет дополнительный барьер памяти, который обеспечивает правильное отображение поля любым потоком.

Для final поля non- такой барьер памяти не создается. Таким образом, в общем, вполне возможно, что метод @PostConstruct -annotated инициализирует поле, и это значение не видимо другим потоком, или, что еще хуже, видно, когда конструктор еще только частично выполнен.

Означает ли это, что вам всегда нужно помечать final поля non- как энергозависимые?

Короче да. Если к полю могут обращаться разные потоки, вы делаете это. Не делайте ту же ошибку, что я сделал, когда думал только об этом в течение нескольких секунд (спасибо Jk1 за исправление) и мыслите с точки зрения последовательности выполнения вашего кода Java. Вы можете подумать, что контекст приложения Spring загружается в один поток. Это означает, что у загрузочного потока не будет проблем с изменчивым полем non-. Таким образом, вы можете подумать, что все в порядке, если вы не предоставляете контекст приложения другому потоку, пока он не будет полностью инициализирован, то есть вызван аннотированный метод. Думая так, вы могли бы предположить, что другие потоки не имеют возможности кэшировать неправильное значение поля, если вы не измените поле после этой начальной загрузки.

Напротив, скомпилированному коду разрешено переупорядочивать инструкции, т. @PostConstruct Даже если метод @PostConstruct -annotated вызывается до того, как связанный бин будет представлен другому потоку в вашем Java-коде, это отношение "до того" не обязательно сохраняется в скомпилированный код во время выполнения. Таким образом, другой поток может всегда считывать и кэшировать volatile поле non-, пока оно еще не инициализировано вообще или даже частично инициализировано. Это может привести к незначительным ошибкам, и в документации Spring, к сожалению, не упоминается об этом. Такие детали JMM являются причиной, почему я лично предпочитаю final поля и конструктор.

Обновление: Согласно этому ответу в другом вопросе, существуют сценарии, в которых отсутствие пометки поля как volatile будет по-прежнему давать действительные результаты. Я исследовал это немного дальше, и среда Spring гарантирует, по сути, определенное количество событий, прежде чем безопасность из коробки. Взгляните на JLS в отношениях "происходит до", где четко говорится:

Разблокировка на мониторе происходит перед каждой последующей блокировкой на этом мониторе.

Среда Spring использует это. Все бины хранятся на одной карте, и Spring получает определенный монитор каждый раз, когда бин регистрируется или извлекается из этой карты. В результате тот же монитор разблокируется после регистрации полностью инициализированного компонента и блокируется перед извлечением этого компонента из другого потока. Это вынуждает этот другой поток уважать отношение "происходит до", которое отражается в порядке выполнения вашего Java-кода. Таким образом, если вы загрузите bean-компонент один раз, все потоки, которые обращаются к полностью инициализированному bean-компоненту, будут видеть это состояние до тех пор, пока они будут обращаться к bean-объекту каноническим способом (т.е. Явным извлечением путем запроса контекста приложения или автоматического переноса). Это делает, например, безопасным @PostConstruct метода или использование метода @PostConstruct даже без объявления поля volatile. На самом деле вам следует избегать volatile полей, так как они вводят накладные расходы времени выполнения для каждого чтения, что может быть болезненным при доступе к полю в циклах и потому, что ключевое слово сигнализирует о неправильном намерении. (Между прочим, насколько мне известно, среда Akka применяет аналогичную стратегию, в которой Akka, кроме Spring, выделяет некоторые аспекты проблемы.)

Эта гарантия, однако, предоставляется только для извлечения bean-компонента после его начальной загрузки. Если вы изменяете volatile поле non- после его начальной загрузки или если вы теряете ссылку на компонент во время его инициализации, эта гарантия больше не применяется.

Проверьте эту старую запись в блоге, которая описывает эту функцию более подробно. По-видимому, эта функция не задокументирована, о чем знают даже пользователи Spring (но долгое время ничего не делали).

Ответ 2

Должен ли я беспокоиться о том, что некоторые данные записывают видимость в другие beans и отмечают, что они нестабильны?

Я не вижу причин, почему вы не должны. Spring Framework не предоставляет никаких дополнительных гарантий безопасности потока при вызове @PostConstruct, поэтому обычные проблемы видимости могут все же произойти. Общим подходом было бы объявить someData final, но если вы хотите изменить поле несколько раз, то оно явно не подходит.

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

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

Обновление:. Согласно всестороннему исследованию, проведенному компанией @raphw Spring, хранится singleton beans на карте, защищенной монитором. Это действительно так, как мы видим из исходного кода org.springframework.beans.factory.support.DefaultSingletonBeanRegistry:

public Object getSingleton(String beanName, ObjectFactory singletonFactory) {
    Assert.notNull(beanName, "'beanName' must not be null");
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        ...
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
}

Этот может предоставить вам свойства безопасности потока на @PostConstruct, но я не считаю это достаточной гарантией по ряду причин:

  • Это влияет только на beans, не предоставляя никаких гарантий для beans других областей: запрос, сеанс, глобальный сеанс, случайную область действия прототипа, пользовательские области видимости (да, вы можете создать один самостоятельно).

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

    public class Entity {
    
        public Object data;
    
        public synchronized void setData(Object data) {
           this.data = data;
        }
    }
    
  • Последнее, но не менее важное: этот внутренний монитор, о котором мы говорим, представляет собой деталь реализации. Будучи недокументированным, он не гарантируется навсегда и может быть изменен без дополнительного уведомления.

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

Ответ 3

SomeService класс, как это определено в вопросе не потокобезопасна. Чтобы сделать класс потокобезопасным, вам необходимо:

  1. Сделайте поле someData volatile.
  2. Убедитесь, что класс Data является поточно-ориентированным.
  3. Убедитесь, что экземпляры SomeService созданы безопасно: https://www.ibm.com/developerworks/library/j-jtp0618/index.html.

Если поле someData не является volatile, но условия 2 и 3 выполняются, тогда код (в отличие от класса) все еще может быть поточно-ориентированным, но только в том случае, если выполняются другие условия, которые находятся вне контроля класса. Самое безопасное - сделать класс потокобезопасным. Тогда вы можете использовать класс в любой ситуации, и он гарантированно всегда будет потокобезопасным.

Spring - это классная среда, но одним из ее ОГРОМНЫХ недостатков является то, что документация редко затрагивает ключевые проблемы с безопасностью потоков.