Double-checked locking без volatile — баг, который не воспроизводится на тесте

Классический паттерн ленивой инициализации синглтона кандидаты часто пишут по памяти — и забывают одну деталь, которая всё ломает.

public class Singleton { private static Singleton instance;

public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

Без volatile на поле instance этот код может отдать другому потоку ссылку на частично инициализированный объект. Создание объекта — это не одна атомарная операция: выделяется память, вызывается конструктор, только потом полю присваивается ссылка. JIT и процессор могут переставить эти шаги местами, если между ними нет барьера памяти.

Поток B в первой проверке if (instance == null) видит ненулевую ссылку, потому что она уже присвоена, но конструктор ещё не дописал состояние объекта. B получает instance с полями в значениях по умолчанию вместо тех, что задал конструктор.

Почему это не ловится на тесте: на x86 с текущими JIT-оптимизациями переупорядочивание такого рода происходит редко, и однопоточный или слабонагруженный тест этого не покажет. Баг всплывает под нагрузкой, на другой архитектуре или после изменения версии JVM — то есть ровно там, где его сложнее всего диагностировать.

Дальше спросят: как исправить одним словом — добавить volatile к полю instance, это восстанавливает happens-before между записью в конструкторе и публикацией ссылки. Ещё спросят про альтернативы: holder-класс с ленивой инициализацией через static-блок или enum-синглтон, где корректность гарантирует сама JVM без ручной синхронизации.

Если double-checked locking — то только с volatile. Без него код компилируется, проходит тесты и падает в проде под нагрузкой, где такие вещи ловить дольше всего.

Тренажёр: 600 вопросов, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки