Два потока одновременно впервые обращаются к классу со статическим состоянием: какую гарантию даёт инициализация класса?
Инициализация класса выполняется ровно один раз, под управлением JVM. Если один поток выполняет её, другие потоки, которым требуется инициализированный класс, ждут завершения этой процедуры и после успешного завершения видят результаты всех записей, сделанных в инициализаторе.
Это гарантирует безопасную публикацию состояния, созданного во время инициализации класса. Однако последующие изменения общего изменяемого статического состояния автоматически не становятся потокобезопасными.
Ручная ленивая инициализация общего объекта требует корректной синхронизации: без неё возможны гонки, частично опубликованный объект и лишние блокировки. JVM предоставляет специальную гарантию для инициализации классов, чтобы код статических инициализаторов выполнялся однократно и согласованно между потоками.
На этой гарантии основан, например, идиоматический шаблон держателя для ленивого singleton. Он позволяет отложить создание объекта до первого обращения к вспомогательному классу без явной блокировки в методе доступа.
Если два потока одновременно выполняют код, который впервые использует один класс, нельзя допустить параллельный запуск его статического инициализатора. Иначе статические поля могли бы быть записаны несколько раз, а читающие потоки — увидеть незавершённое состояние.
Неверно также считать, что эта гарантия распространяется на все будущие операции со статическими полями. После завершения инициализации обычные записи в изменяемое статическое поле требуют отдельной синхронизации, например volatile, блокировки или потокобезопасной структуры данных.
JVM выбирает один поток для выполнения инициализации класса. Пока инициализация не завершена, другие потоки, которым нужен результат инициализации, не продолжают использование этого класса; после успешного завершения они получают согласованное состояние, опубликованное инициализатором.
Записи, выполненные во время инициализации, становятся видимыми потокам, которые используют класс после её завершения. Поэтому статическое поле, созданное и присвоенное в инициализаторе, может безопасно публиковать полностью сконструированный объект.
Инициализация запускается не при любом упоминании имени класса. Например, обращение к константному статическому полю примитивного типа или String, значение которого является константным выражением, обычно не требует инициализации класса. Это важно при анализе порядка выполнения побочных эффектов.
Если инициализатор завершается исключением, нормальная инициализация не считается успешной: класс помечается как находящийся в ошибочном состоянии, а последующие попытки активного использования обычно приводят к ошибке инициализации. Поэтому исключения и побочные эффекты в статических инициализаторах требуют особой осторожности.
Гарантия не защищает от утечки ссылки на ещё не завершённый объект внутри самого инициализатора. Также она не делает потокобезопасными методы, которые позже изменяют опубликованный объект или статическое поле.
Минимальный вариант ленивой публикации выглядит так:
Вложенный класс инициализируется только при обращении к Holder.INSTANCE. JVM выполняет его инициализацию один раз, поэтому объект создаётся лениво и безопасно публикуется без ручной синхронизации метода instance.
В приложении требовался ленивый singleton конфигурации, создание которого включало чтение и разбор файла. Рассматривались синхронизация каждого вызова accessor-метода, двойная проверка с volatile и шаблон держателя.
Синхронизация каждого вызова была простой и надёжной, но добавляла проверку блокировки на каждый доступ. Двойная проверка могла обеспечить корректность, однако требовала строго правильной реализации и volatile-ссылки; ошибка в деталях легко приводила к проблеме публикации.
Выбрали статический держатель: создание выполняется лениво, однократно и с гарантией инициализации класса, а обычный путь чтения не содержит явной блокировки. При этом операции обновления конфигурации всё равно сделали отдельной синхронизированной процедурой, поскольку гарантия инициализации не защищает последующие изменения.
Не обязательно. Константное статическое поле примитивного типа или String, заданное константным выражением, может быть встроено компилятором в использующий код. В таком случае активного использования класса не происходит, и статический инициализатор класса не обязан запускаться.
Это отличается от чтения обычного статического поля или вызова статического метода: такие действия обычно требуют инициализации класса перед продолжением.
Нет. Она безопасно публикует состояние, сформированное до завершения инициализации, но не синхронизирует будущие операции.
Если после публикации один поток меняет поля объекта, а другие читают их, нужны собственные гарантии видимости и атомарности: volatile для подходящего одиночного состояния, блокировка, атомарные классы или потокобезопасная коллекция. Особенно важно, что безопасная публикация объекта не превращает составную последовательность операций над ним в одну атомарную операцию.
Повторного параллельного запуска инициализатора не происходит. Для того же потока допустимо обращение к уже выполняющейся инициализации, но логика может получить частично присвоенное состояние или попасть в рекурсию; другой поток будет ждать завершения инициализации.
Поэтому сложную работу, взаимные зависимости классов и вызовы внешнего кода из статических инициализаторов следует минимизировать. Инициализатор должен быть коротким, детерминированным и не допускать утечки частично созданного состояния.