Как проверить потокобезопасность ленивого синглтона в Java
Что проверяют
Понимаете ли вы модель памяти Java, а не только слово synchronized. Интервьюер ждёт, заметите ли вы, что многопоточный тест такой синглтон почти никогда не уронит, и что корректность здесь доказывается рассуждением. Следом просят написать правильную версию и сравнить её с альтернативами.
Как отвечать
Начните с кода. Double-checked locking работает только с private static volatile LazySingleton instance. Без volatile запись ссылки может стать видимой другому потоку раньше, чем отработал конструктор, и тот получит объект с неинициализированными полями. volatile даёт отношение happens-before между записью и чтением.
Что можно сделать тестами:
- гонка при создании:
ExecutorServiceна десятки потоков, общийCountDownLatch, чтобы все стартовали одновременно, экземпляры собираются вConcurrentHashMap.newKeySet(), в конце проверка, что он один. Счётчик вызовов конструктора наAtomicIntegerи искусственная задержка в конструкторе расширяют окно гонки; - jcstress: инструмент из OpenJDK многократно гоняет конкурентные сценарии и ловит эффекты переупорядочивания, но работает на JVM, а не на ART;
- статический анализ: SpotBugs и Error Prone находят double-checked locking без
volatile.
Потом назовите варианты, которые проще проверить на ревью:
- holder-класс:
private static class Holder { static final LazySingleton INSTANCE = new LazySingleton(); }. Ленивость и потокобезопасность обеспечивает сама инициализация класса; - enum: защищает и от рефлексии, и от сериализации;
- Kotlin:
objectилиby lazy, у которого режим по умолчаниюLazyThreadSafetyMode.SYNCHRONIZED.
Где спотыкаются
Показывают double-checked locking без volatile и считают его рабочим, потому что тест с десятью потоками всегда печатает один адрес.
Путают видимость с атомарностью: думают, что synchronized вокруг создания решает всё, хотя первое чтение поля идёт вне блока.
Уверяют, что прогон теста доказывает корректность. Зелёный многопоточный тест говорит только о том, что баг не проявился в этот раз.