← Назад к вопросам

Что такое Double-Checked Locking?

3.0 Senior🔥 151 комментариев
#SOLID и паттерны проектирования#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Double-Checked Locking: оптимизация синхронизации

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

Классический пример: Lazy Initialization

// Наивный способ - синхронизируем ВСЕ операции
public class Singleton {
    private static Singleton instance;
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Проблема: После инициализации instance все последующие вызовы getInstance() входят в синхронизированный метод, что дорого.

Double-Checked Locking решение

public class Singleton {
    private static volatile Singleton instance;  // ← ВАЖНО: volatile!
    
    public static Singleton getInstance() {
        // Первая проверка БЕЗ блокировки (быстро)
        if (instance == null) {
            synchronized (Singleton.class) {
                // Вторая проверка ПОД блокировкой (безопасно)
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Как это работает:

  1. Первая проверка (без блокировки): если instance != null → возвращаем
  2. Вторая проверка (с блокировкой): ещё раз проверяем, может другой поток создал
  3. Инициализация только один раз

Почему нужен volatile?

// ❌ БЕЗ volatile - МОЖЕТ НЕ РАБОТАТЬ!
private static Singleton instance;  // Опасно!

// ✓ С volatile - ПРАВИЛЬНО
private static volatile Singleton instance;  // Безопасно

Причина: volatile гарантирует visibility между потоками:

  • Изменения видны сразу всем потокам
  • Предотвращает кэширование значения в регистрах CPU
  • Работает с Java Memory Model
public class VolatileExample {
    public static void main(String[] args) throws InterruptedException {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        
        System.out.println(s1 == s2);  // true (один и тот же объект)
        // Поток 1 создал объект, Поток 2 увидел его благодаря volatile
    }
}

Детальное объяснение

public class DCLDemo {
    private static volatile DCLDemo instance;
    private String data;
    
    private DCLDemo() {
        // Дорогая инициализация
        this.data = initializeData();
    }
    
    public static DCLDemo getInstance() {
        // БЫСТРЫЙ ПУТЬ (99% времени)
        if (instance == null) {  // ← Читаем без блокировки
            // Только сюда входим при первом вызове
            synchronized (DCLDemo.class) {
                // БЕЗОПАСНЫЙ ПУТЬ
                if (instance == null) {  // ← Проверка под блокировкой
                    instance = new DCLDemo();  // ← Создаём ровно один раз
                }
            }
        }
        return instance;
    }
    
    private String initializeData() {
        // Дорогие операции
        return "initialized";
    }
}

Многопоточный сценарий

public class ThreadScenario {
    public static void main(String[] args) throws InterruptedException {
        // Запускаем несколько потоков
        Runnable task = () -> {
            DCLDemo instance = DCLDemo.getInstance();
            System.out.println(Thread.currentThread().getName() + ": " + instance);
        };
        
        // Первый поток
        Thread t1 = new Thread(task, "Thread-1");
        // Второй поток (может придти пока t1 создаёт объект)
        Thread t2 = new Thread(task, "Thread-2");
        // Третий поток
        Thread t3 = new Thread(task, "Thread-3");
        
        t1.start();
        t2.start();
        t3.start();
        
        t1.join();
        t2.join();
        t3.join();
        
        // Все потоки получат ОДН И ТОТ ЖЕ объект!
    }
}

Производительность: сравнение

public class PerformanceComparison {
    
    // Вариант 1: Synchronized метод (медленно после инициализации)
    public static class SynchronizedSingleton {
        private static SynchronizedSingleton instance;
        
        public static synchronized SynchronizedSingleton getInstance() {
            if (instance == null) {
                instance = new SynchronizedSingleton();
            }
            return instance;
        }
    }
    
    // Вариант 2: Double-Checked Locking (быстро после инициализации)
    public static class DCLSingleton {
        private static volatile DCLSingleton instance;
        
        public static DCLSingleton getInstance() {
            if (instance == null) {
                synchronized (DCLSingleton.class) {
                    if (instance == null) {
                        instance = new DCLSingleton();
                    }
                }
            }
            return instance;
        }
    }
    
    // Вариант 3: Class Loader (самый быстрый, рекомендуется)
    public static class ClassLoaderSingleton {
        private static class Holder {
            static final ClassLoaderSingleton INSTANCE = new ClassLoaderSingleton();
        }
        
        public static ClassLoaderSingleton getInstance() {
            return Holder.INSTANCE;  // ← Class Loader гарантирует thread safety
        }
    }
}

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

Используй DCL:

  • Lazy initialization в многопоточном контексте
  • Когда инициализация дорогая
  • Когда нужна быстрая работа после инициализации
// Пример: инициализация конфигурации
public class ConfigManager {
    private static volatile ConfigManager instance;
    private Map<String, String> config;
    
    public static ConfigManager getInstance() {
        if (instance == null) {
            synchronized (ConfigManager.class) {
                if (instance == null) {
                    instance = new ConfigManager();
                    instance.loadConfiguration();  // Дорогая операция I/O
                }
            }
        }
        return instance;
    }
    
    private void loadConfiguration() {
        // Читаем конфиг из файла
        config = loadFromFile("config.properties");
    }
}

Но ЛУЧШЕ используй:

  • Class Loader паттерн (Bill Pugh Singleton)
  • Enum Singleton (самый безопасный)
  • Spring @Bean (для managed beans)
// ✓ ЛУЧШИЙ СПОСОБ - Enum
public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // ...
    }
}

// Использование
Singleton.INSTANCE.doSomething();

Потенциальные проблемы

// ❌ БЕЗ volatile могут быть проблемы
private static Singleton instance;  // Может не работать корректно

// ✓ volatile обязателен
private static volatile Singleton instance;  // Правильно

// ❌ Только одна проверка - race condition
if (instance == null) {
    instance = new Singleton();  // Могут создать два объекта!
}

// ✓ Двойная проверка - безопасно
if (instance == null) {
    synchronized (Singleton.class) {
        if (instance == null) {
            instance = new Singleton();  // Ровно один объект
        }
    }
}

Double-Checked Locking — это важный паттерн для оптимизации многопоточных приложений, но его нужно использовать осторожно, убедившись, что volatile добавлен правильно.