Что такое паттерн Double-Checked Locking и когда его применяют?

«Что такое паттерн Double-Checked Locking и когда его применяют?» — вопрос из категории Паттерны, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Double-Checked Locking (DCL) — это паттерн оптимизации для ленивой инициализации в многопоточной среде, который минимизирует накладные расходы на синхронизацию.

Проблема: Без DCL при ленивой инициализации Singleton приходится синхронизировать каждый вызов getInstance():

// Медленная реализация
public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

Решение с DCL:

public class Singleton {
    // КЛЮЧЕВОЕ: volatile для Java 5+
    private static volatile Singleton instance;

    private Singleton() {
        // приватный конструктор
    }

    public static Singleton getInstance() {
        // Первая проверка (без блокировки)
        if (instance == null) {
            synchronized (Singleton.class) {
                // Вторая проверка (под блокировкой)
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Почему нужен volatile (начиная с Java 5): Без volatile возможна ситуация, когда один поток видит частично инициализированный объект из-за переупорядочивания инструкций:

// Без volatile может произойти:
// 1. Выделение памяти
// 2. Запись ссылки в instance (ещё до инициализации!)
// 3. Инициализация полей
// Другой поток на шаге 2 увидит instance != null и вернёт "сырой" объект

Альтернативы в современных Java:

  1. Holder pattern (рекомендуется):

    public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
    }
  2. Enum Singleton (самый безопасный):

    public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // реализация
    }
    }

Когда использовать DCL:

  • Когда ленивая инициализация критична для производительности
  • В legacy-коде, где нельзя изменить архитектуру
  • Когда Singleton содержит тяжёлые ресурсы, которые не всегда нужны

Когда НЕ использовать:

  • В новых проектах (предпочитайте Holder или Enum)
  • Если не понимаете тонкостей memory model Java
  • Для простых объектов, где overhead синхронизации незначителен