Модель памяти Java (JMM) гарантирует, что как только конструктор завершен, записи в финальные поля становятся видимыми для любого потока, который читает ссылку на объект, при условии, что эта ссылка не покинула область видимости во время создания. Если ссылка this утечет преждевременно — будет передана другому потоку или сохранена в статической коллекции до завершения конструктора — связь happens-before между записью конструктора в финальное поле и чтением другим потоком разрывается. В результате наблюдающий поток может увидеть значение по умолчанию (ноль, false или null), что нарушает очевидную неизменяемость. Безопасная публикация требует, чтобы ни одна ссылка на создаваемый объект не покинула область видимости до завершения создания, что обеспечивает замораживание действий по книге финальных полей до того, как любой поток сможет загрузить ссылку.
Мы столкнулись с этим в системе высокочастотной торговли, где экземпляры Service регистрировались в глобальном ConcurrentHashMap во время своих конструкторов для облегчения поиска. Класс определял финальное long instrumentId, инициализируемое из параметра конструктора, но контролирующие потоки периодически читали ноль, когда запрашивали реестр сразу после создания.
Одно из предложенных решений заключалось в том, чтобы объявить instrumentId как volatile вместо final, надеясь заставить изменения сразу стать видимыми между ядрами. Этот подход гарантировал атомарность и видимость, но отказывался от контракта по неизменяемости и налагал полные затраты на память при каждом чтении, ненужно ухудшая пропускную способность для значения, которое никогда не изменялось после создания, и усложняя понимание состояния объекта.
Еще одно предложение заключалось в синхронизации всех доступов к реестру с помощью synchronized блоков, охватывающих логику конструктора, теоризируя, что блокировки очистят кэши памяти. Хотя это предотвращало гонки данных, это вводило сильную конкуренцию при глобальной блокировке реестра, превращая конкурентную структуру в серийную узкую стенку и нарушая строгие требования к задержке для поглощения рыночных данных.
Мы выбрали паттерн фабрики, который разграничивал создание и регистрацию. Конструктор остался приватным, фабричный метод вызывал new Service(id) полностью, и только после этого публиковал полностью сформированную ссылку на ConcurrentHashMap. Это использовало семантику замораживания финальных полей JMM без накладных расходов на синхронизацию, обеспечивая, что instrumentId был виден сразу после получения.
Изменение устранило аномалии с нулевой видимостью и восстановило ожидаемую задержку в масштабе микросекунд для поиска сервиса, при этом сохранив намерение по неизменяемому дизайну.
Почему final не гарантирует видимость, если я просто публикую ссылку через потокобезопасную коллекцию, такую как ConcurrentHashMap?
Связь happens-before, предоставляемая операциями put и get в ConcurrentHashMap, устанавливает порядок между изменениями внутреннего состояния карты, а не между записями конструктора и публикацией в карте. Если this покидает область видимости во время создания, запись в финальное поле происходит в одном потоке, в то время как публикация карты происходит одновременно, лишая нужной связи happens-before, необходимой для предотвращения переупорядочивания инструкций. Таким образом, поток чтения может наблюдать ссылку через карту до того, как записи конструктора будут сброшены в основную память, наблюдая значение по умолчанию.
Могу ли я исправить это, сделав поле реестра volatile вместо полей объекта?
Пометить ссылку на реестр как volatile гарантирует только, что изменения переменной реестра будут видны, а не внутреннее состояние объектов, которые она содержит. Поскольку проблема заключается во времени записи полей объекта относительно того, когда ссылка становится видимой, volatile на контейнере не устанавливает нужного порядка между конструктором и потребителем объекта. Вы все равно будете наблюдать частично созданные экземпляры.
Предотвращает ли использование synchronized внутри конструктора небезопасную публикацию?
Размещение synchronized на конструкторе или использование его для охраны регистрации предотвращает одновременный доступ других потоков к критической секции, но не препятствует утечке ссылки this, если метод регистрации утечет ссылку другому потоку, работающему вне этой блокировки. JMM специально требует, чтобы ни одна ссылка на объект не покидала область видимости до завершения конструктора, чтобы семантика финальных полей могла сохраняться; синхронизация без надлежащего порядка публикации не может восстановить эту гарантию.