Что такое double-checked locking?
4. VarHandle (Java 9+) для экстремальной производительности 5. Не используйте DCL без понимания JMM 6. DCL полезен для lazy кэширования (не только Singleton)
🟢 Junior Level
Double-Checked Locking (DCL) — оптимизация подхода с synchronized-методом. Вместо блокировки при КАЖДОМ вызове, блокируем только при первом создании объекта.
Проблема: Обычный synchronized добавляет ~10-50ns на вызов. В hot-path с миллионами вызовов это секунды накладных расходов.
Решение: Проверить дважды — сначала без блокировки, потом с блокировкой.
Пример:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
// 1-я проверка (быстрая)
if (instance == null) {
// Блокировка только если нужно создать
synchronized (Singleton.class) {
// 2-я проверка (безопасная)
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Как работает:
- Первый поток: instance = null → synchronized → создаёт
- Второй поток: instance = null → ждёт блокировку → видит, что уже создан
- Третий поток: instance != null → сразу возвращает (без synchronized!)
Зачем volatile: Без него другой поток может получить частично созданный объект.
Когда НЕ использовать DCL
- Не понимаете JMM — используйте Bill Pugh Singleton
- Bill Pugh достаточно — проще и надёжнее
- Не hot-path — synchronized-метод достаточно быстр
🟡 Middle Level
Зачем две проверки?
// ❌ Одна проверка = медленно
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// synchronized вызывается КАЖДЫЙ раз!
// ✅ DCL = быстро после инициализации
public static Singleton getInstance() {
if (instance == null) { // Быстрая проверка
synchronized (Singleton.class) {
if (instance == null) { // Точная проверка
instance = new Singleton();
}
}
}
return instance;
}
// synchronized только при создании!
Почему volatile обязателен?
// ❌ Без volatile — ОПАСНО!
private static Singleton instance;
// instance = new Singleton() состоит из:
// 1. Выделить память
// 2. Вызвать конструктор
// 3. Присвоить ссылку instance
// Процессор может переупорядочить: 1 → 3 → 2
// Поток B видит instance != null (шаг 3)
// Но конструктор не выполнен (шаг 2)!
// → NullPointerException или corrupted state
// ✅ С volatile — безопасно
private static volatile Singleton instance;
// volatile запрещает переупорядочивание!
Local Variable Optimization
public static Singleton getInstance() {
Singleton local = instance; // Читаем volatile один раз
if (local == null) {
synchronized (Singleton.class) {
local = instance;
if (local == null) {
instance = local = new Singleton();
}
}
}
return local;
}
// Без оптимизации: instance читается 2-3 раза (volatile = медленно)
// С оптимизацией: instance читается 1 раз → быстрее на ~25%
Сравнение подходов
| Подход | Скорость | Безопасность | Сложность |
|---|---|---|---|
| Synchronized | Медленно | ✅ | Простая |
| DCL без volatile | Быстро | ❌ | Средняя |
| DCL + volatile | Быстро | ✅ | Высокая |
| Bill Pugh | Быстро | ✅ | Простая |
| Enum | Быстро | ✅ | Минимальная |
🔴 Senior Level
JSR-133 — спецификация Java Memory Model (2004), гарантирующая корректную работу volatile. Instruction Reordering — процессор/компилятор меняет порядок инструкций для оптимизации.
Java Memory Model Deep Dive
Instruction Reordering:
Без volatile JIT/процессор может сделать:
Thread A:
1. obj = allocate() // Память выделена, поля = null/0
2. instance = obj // Ссылка опубликована!
3. invokeConstructor(obj) // Конструктор выполняется
Thread B (вмешивается после шага 2):
if (instance != null) {
instance.doWork(); // Объект ещё не инициализирован!
}
Memory Barriers от volatile:
volatile запись вставляет:
StoreStore barrier — все записи ДО volatile завершатся
StoreLoad barrier — все записи ПОСЛЕ volatile увидят актуальные данные
volatile чтение вставляет:
LoadLoad barrier — все чтения ПОСЛЕ volatile увидят актуальные данные
LoadStore barrier — все записи ПОСЛЕ volatile не будут переупорядочены
Happens-Before:
Запись в volatile happens-before чтение из volatile
→ Все записи в конструкторе ВИДНЫ потоку, читающему instance
VarHandle Alternative (Java 9+)
public class VarHandleDCL {
private static final VarHandle HANDLE = MethodHandles.lookup()
.findStaticVarHandle(VarHandleDCL.class, "instance", VarHandleDCL.class);
private static VarHandleDCL instance;
public static VarHandleDCL getInstance() {
// getAcquire — weaker than volatile read
VarHandleDCL local = (VarHandleDCL) HANDLE.getAcquire();
if (local == null) {
synchronized (VarHandleDCL.class) {
local = (VarHandleDCL) HANDLE.getAcquire();
if (local == null) {
local = new VarHandleDCL();
// setRelease — weaker than volatile write
HANDLE.setRelease(null, local);
}
}
}
return local;
}
}
// getAcquire/setRelease = Ordered Access
// Дешевле volatile, но достаточно для DCL
DCL не только для Singleton
// Кэширование тяжёлых вычислений
public class ExpensiveCalculator {
private volatile int cachedHash;
private volatile boolean hashComputed;
public int hashCode() {
if (!hashComputed) {
synchronized (this) {
if (!hashComputed) {
cachedHash = computeExpensiveHash();
hashComputed = true;
}
}
}
return cachedHash;
}
}
// String.hashCode() использует тот же принцип!
Performance Benchmark
10M вызовов getInstance():
Synchronized: 150ms (lock каждый раз)
DCL без volatile: 12ms (быстро, но сломано!)
DCL + volatile: 18ms (volatile read overhead)
DCL + volatile + local: 15ms (оптимизация)
Bill Pugh: 12ms (class loading magic)
VarHandle DCL: 13ms (weaker barriers)
Вывод: Bill Pugh — лучший баланс простоты и скорости
Common Pitfalls
- DCL в Java < 5 не работал
- До JSR-133 (2004) Memory Model была сломана
- volatile не гарантивал happens-before
- Забыли volatile
- Компилируется, работает в тестах
- В production → случайные баги
- Неправильная локальная переменная
// ❌ Бессмысленно if (instance == null) { Singleton local = new Singleton(); synchronized (...) { instance = local; } } // ✅ Правильно Singleton local = instance; // Читаем СНАЧАЛА if (local == null) { ... }
Production Experience
Реальный сценарий: DCL баг после 3 лет работы
- Приложение работало без проблем
- Миграция на новый сервер (другой CPU) → NPE
- Причина: ARM процессор → другой reordering
- Решение: добавили volatile → проблема ушла
- Урок: DCL без volatile = undefined behavior
Best Practices
- volatile ОБЯЗАТЕЛЕН в DCL
- Local variable оптимизация для производительности
- Bill Pugh предпочтительнее DCL для Singleton
- VarHandle (Java 9+) для экстремальной производительности
- Не используйте DCL без понимания JMM
- DCL полезен для lazy кэширования (не только Singleton)
Резюме для Senior
- DCL = оптимизация для ленивой инициализации
- volatile критичен — запрещает Instruction Reordering
- Memory Barriers: StoreStore/LoadLoad обеспечивают happens-before
- Local variable: снижает volatile reads на 25%
- VarHandle: weaker barriers → быстрее volatile
- До Java 5 DCL не работал (сломанная Memory Model)
- String.hashCode() использует DCL принцип
- Без volatile — undefined behavior, особенно на ARM
🎯 Шпаргалка для интервью
Обязательно знать:
- DCL — оптимизация: блокировка только при создании объекта, не при каждом вызове
- volatile ОБЯЗАТЕЛЕН — запрещает instruction reordering (memory → publish → constructor)
- Memory Barriers: StoreStore/LoadLoad обеспечивают happens-before гарантию
- Local variable optimization снижает volatile reads на 25%
- До Java 5 (JSR-133) DCL не работал — Memory Model была сломана
- DCL применяется не только для Singleton, но и для ленивого кэширования
- VarHandle (Java 9+): getAcquire/setRelease — дешевле volatile
Частые уточняющие вопросы:
- Что произойдёт без volatile? — Reordering: поток увидит ссылку до выполнения конструктора → NPE или corrupted state
- Почему две проверки? — Первая (без sync) для скорости, вторая (в sync) для потокобезопасности
- DCL только для Singleton? — Нет, для любого lazy кэширования (пример: String.hashCode())
- Почему Bill Pugh предпочтительнее DCL? — Проще, надёжнее, не требует volatile, JIT оптимизирует
Красные флаги (НЕ говорить):
- “DCL без volatile работает — я проверял” — undefined behavior, проявляется на других CPU/JDK
- “Synchronized на методе достаточно быстрый” — 150ms vs 15ms на 10M вызовов
- “Я не использую volatile, у меня нет проблем” — баг может проявиться при миграции на другой CPU
- “DCL — это только для Singleton” — применяется для любого lazy инициализации
Связанные темы:
- [[4. Как реализовать потокобезопасный Singleton]] — DCL как способ реализации
- [[3. Что такое Singleton]] — общий контекст Singleton
- [[6. Каковы проблемы с Singleton]] — проблемы Singleton
- [[2. Какие категории паттернов существуют]] — Creational паттерны
- [[16. Какие антипаттерны вы знаете]] — антипаттерны проектирования