Сравните гарантии JMM для final-полей и обычных полей объекта, опубликованного без синхронизации.
Если конструктор объекта завершился нормально и ссылка на объект не утекла из конструктора, final-поля имеют специальную гарантию Java Memory Model: поток, получивший ссылку, должен увидеть их корректно инициализированные даже без отдельной синхронизации. Для обычных полей такой гарантии нет: поток может увидеть устаревшие значения или не наблюдать корректную публикацию состояния объекта.
Это не делает объект автоматически неизменяемым и потокобезопасным. Изменения после конструктора, а также доступ к изменяемым объектам, связанным с обычным состоянием, требуют отдельного механизма безопасной публикации и синхронизации.
Модель памяти Java должна одновременно допускать оптимизации компилятора и процессора и давать разработчику предсказуемые правила видимости между потоками. Без специальных правил даже создание неизменяемого объекта пришлось бы всегда сопровождать полноценной синхронизацией.
Особая семантика final-полей решает именно эту проблему: она позволяет безопаснее использовать неизменяемые объекты после передачи ссылки между потоками. При этом она не заменяет общую гарантию happens-before для произвольного изменяемого состояния.
Рассмотрим объект конфигурации, который один поток создаёт, а затем помещает в общее поле без volatile, блокировки или другого механизма публикации. Другой поток читает это поле и использует объект.
Если поля объекта обычные, чтение ссылки не гарантирует корректную видимость результатов конструктора. Возможны устаревшие значения, а полагаться на порядок исходных операций программы в такой ситуации нельзя.
Для final действует более сильное правило, но только при соблюдении условий: поле присвоено в конструкторе, конструктор завершился нормально, и ссылка this не была опубликована до завершения конструирования. Нарушение этих условий лишает разработчика соответствующих гарантий.
Специальная семантика final распространяется на значение поля, установленное в конструкторе. Если другой поток получил ссылку на полностью сконструированный объект, он должен увидеть корректное значение такого final-поля даже при отсутствии синхронизации.
Для обычного поля одной лишь записью ссылки безопасная публикация не создаётся. Нужен механизм, формирующий отношение happens-before: например, запись и чтение volatile-поля, захват и освобождение одного монитора, публикация через потокобезопасную коллекцию или корректное завершение задачи с последующим получением результата.
Если final-поле является ссылкой на объект или массив, специальная гарантия может распространяться на состояние, доступное через эту ссылку на момент завершения конструктора. Она не распространяется на последующие изменения этого объекта и не превращает его в неизменяемый или потокобезопасный.
Если другой поток уже увидел ненулевую ссылку config, значение timeout имеет специальную гарантию корректной инициализации. Для retries такой гарантии нет. Само чтение ссылки также не обязано быстро увидеть новую запись, поэтому пример не является общим способом безопасной публикации.
Значимое ограничение — запрет утечки this из конструктора: вызов внешнего кода, передача ссылки в другой поток или регистрация объекта в общем хранилище до завершения конструктора могут нарушить ожидаемые гарантии. Также отражённое изменение final-поля, низкоуровневые операции и ошибки жизненного цикла объекта не следует рассматривать как обычный сценарий семантики final.
Сервис создаёт объект конфигурации с final-параметрами и обычным счётчиком диагностических повторов, после чего публикует ссылку через незаблокированное статическое поле. В редком случае рабочий поток получает ссылку, но видит некорректное значение счётчика; попытка объявить только счётчик volatile не решает проблему публикации остальных полей.
Рассматривались варианты:
final — минимальные затраты, но обычные поля и видимость ссылки остаются проблемой;final — хорошо подходит для неизменяемой конфигурации, но не подходит для изменяемого счётчика;volatile-ссылку — даёт видимость записей, выполненных до публикации, но не делает последующие изменения полей автоматически безопасными;Выбран вариант: неизменяемую конфигурацию сделать полностью final, а ссылку публиковать через volatile или безопасный механизм инициализации. Изменяемый счётчик хранится отдельно и обновляется через атомарный тип либо под защитой блокировки. Это разделяет безопасную публикацию конфигурации и синхронизацию изменяемой статистики, устраняя гонку без лишней блокировки чтения конфигурации.
1. Сохраняется ли гарантия final, если конструктор передал this в другой поток?
Нет, полагаться на неё нельзя. Если ссылка на объект стала доступна другому потоку до завершения конструктора, этот поток может наблюдать объект в промежуточном состоянии, а специальная гарантия для final нарушается условиями применения. Запуск фоновой задачи из конструктора, регистрация слушателя или передача this в вызываемый метод — типичные формы такой утечки.
2. Делает ли наличие final объект неизменяемым и потокобезопасным?
Нет. final запрещает переназначение самой переменной-ссылки, но не запрещает изменение объекта, на который она указывает, если этот объект изменяем. Кроме того, обычные поля самого объекта могут меняться после конструктора; для таких изменений нужны синхронизация, volatile, атомарные классы или другой подход, соответствующий алгоритму.
3. Достаточно ли сделать final ссылку на изменяемый список?
Недостаточно для безопасной совместной работы со списком. Начальное состояние, сформированное до завершения конструктора, имеет более сильные гарантии видимости через final-ссылку, но последующие добавления и удаления требуют собственной синхронизации. Если список должен оставаться неизменяемым, следует использовать неизменяемую структуру или защитную копию; если он изменяется, нужен потокобезопасный контейнер либо единый протокол доступа.