Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Для чего нужен Double Check?
Double-checked locking (двойная проверка) — это паттерн оптимизации для ленивой инициализации объектов в многопоточной среде. Он решает проблему между производительностью и потокобезопасностью, минимизируя затраты на синхронизацию после первого создания объекта.
Основная проблема
Рассмотрим наивный подход к ленивой инициализации:
public class Logger {
private static Logger instance;
// ❌ НЕПРАВИЛЬНО - не потокобезопасно
public static Logger getInstance() {
if (instance == null) { // Потокобезопасность нарушена!
instance = new Logger();
}
return instance;
}
}
// Проблема в многопоточной среде:
// Поток 1: Проверяет instance == null -> true, начинает создание
// Поток 2: Проверяет instance == null -> true, начинает создание
// Результат: Два экземпляра Logger вместо одного!
Решение 1: Синхронизация всего метода (неоптимально)
public class Logger {
private static Logger instance;
// ✅ Потокобезопасно, но МЕДЛЕННО
public synchronized static Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
}
// Проблема: synchronized блокирует КАЖДЫЙ вызов!
// После первой инициализации:
// - instance != null
// - Но все равно нужно взять lock для проверки
// - В высоконагруженной системе это огромный overhead
// Performance impact:
// 100 потоков вызывают getInstance():
// 1 поток: создает объект, берет lock
// 99 потоков: ждут lock, проверяют instance != null, отпускают lock
// Результат: конкуренция (contention) на lock
Решение 2: Double-checked locking (оптимально)
Это двойная проверка: без lock и с lock:
public class Logger {
private static volatile Logger instance; // ✅ VOLATILE - критично!
// ✅ ПРАВИЛЬНО - Double-checked locking
public static Logger getInstance() {
// Первая проверка БЕЗ lock (быстро)
if (instance == null) {
synchronized (Logger.class) { // Lock только при необходимости
// Вторая проверка С lock (потокобезопасно)
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
}
Как это работает:
Сценарий 1: instance уже инициализирован
┌─────────────────────────────────────────────┐
│ Поток A: instance = Logger(...) │ (только ОДИН раз)
├─────────────────────────────────────────────┤
│ Поток B: if (instance == null) → FALSE │ (без lock!)
│ Поток C: if (instance == null) → FALSE │ (без lock!)
│ Поток D: if (instance == null) → FALSE │ (без lock!)
└─────────────────────────────────────────────┘
Все читают уже инициализированный объект БЕЗ затрат на synchronization
Сценарий 2: instance ещё не инициализирован (только один раз)
┌─────────────────────────────────────────────┐
│ Поток A: if (instance == null) → TRUE │
│ Поток A: synchronized(this) { берет lock } │
│ Поток A: if (instance == null) → TRUE │ (вторая проверка)
│ Поток A: instance = new Logger() │
│ Поток A: { отпускает lock } │
├─────────────────────────────────────────────┤
│ Поток B: if (instance == null) → FALSE │ (уже создан)
│ Поток B: возвращает объект БЕЗ lock │
└─────────────────────────────────────────────┘
Почему volatile НЕОБХОДИМ?
Вот самый важный момент! Без volatile double-checked locking НЕ работает надежно:
// ❌ ОПАСНО - БЕЗ volatile
public class UnsafeLogger {
private static Logger instance; // БЕЗ volatile!
public static Logger getInstance() {
if (instance == null) {
synchronized (UnsafeLogger.class) {
if (instance == null) {
// Проблема с переупорядочением инструкций (instruction reordering)
Logger temp = new Logger(); // 1. Создание объекта
instance = temp; // 2. Присвоение ссылки
// JVM может переупорядочить как:
// 1. instance = null (выделение памяти)
// 2. (инициализация fields)
// 3. instance указывает на объект
// Поток B может видеть instance != null, но объект еще не инициализирован!
}
}
}
return instance; // Может быть не полностью инициализирован!
}
}
// ✅ ПРАВИЛЬНО - С volatile
public class SafeLogger {
private static volatile Logger instance; // volatile гарантирует порядок!
public static Logger getInstance() {
if (instance == null) {
synchronized (SafeLogger.class) {
if (instance == null) {
instance = new Logger();
// volatile гарантирует, что все изменения видны другим потокам
}
}
}
return instance;
}
}
Полный пример
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private String connectionString;
// Приватный конструктор (предотвращает прямое инстанцирование)
private DatabaseConnection() {
// Дорогостоящая инициализация
this.connectionString = "jdbc:mysql://localhost:3306/mydb";
System.out.println("Database connection initialized");
}
// Double-checked locking
public static DatabaseConnection getInstance() {
// Первая проверка (без lock)
if (instance == null) {
// Вторая проверка (с lock)
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public void executeQuery(String sql) {
System.out.println("Executing: " + sql);
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);
// 10 потоков пытаются получить Singleton
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
DatabaseConnection db = DatabaseConnection.getInstance();
System.out.println("Got instance: " + System.identityHashCode(db));
db.executeQuery("SELECT * FROM users");
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
// Все потоки получат ТОТ ЖЕ экземпляр
}
}
// Вывод:
// Database connection initialized (только ОДИН раз)
// Got instance: <hash1>
// Got instance: <hash1>
// Got instance: <hash1>
// ... (все имеют одинаковый hash)
Производительность: Сравнение подходов
public class PerformanceComparison {
static class NaiveApproach {
// ❌ Не потокобезопасно
private static Object instance;
public static Object get() {
if (instance == null) {
instance = new Object();
}
return instance;
}
}
static class SynchronizedMethod {
// ✅ Потокобезопасно, но медленно
private static Object instance;
public static synchronized Object get() {
if (instance == null) {
instance = new Object();
}
return instance;
}
}
static class DoubleChecked {
// ✅ Потокобезопасно и быстро
private static volatile Object instance;
public static Object get() {
if (instance == null) {
synchronized (DoubleChecked.class) {
if (instance == null) {
instance = new Object();
}
}
}
return instance;
}
}
// Бенчмарк (после инициализации)
public static void benchmark() throws Exception {
// Подготовка
SynchronizedMethod.get();
DoubleChecked.get();
// Тест 1: Synchronized method (100 млн вызовов)
long start = System.nanoTime();
for (int i = 0; i < 100_000_000; i++) {
SynchronizedMethod.get();
}
long syncTime = (System.nanoTime() - start) / 1_000_000;
// Тест 2: Double-checked locking (100 млн вызовов)
start = System.nanoTime();
for (int i = 0; i < 100_000_000; i++) {
DoubleChecked.get();
}
long doubleCheckedTime = (System.nanoTime() - start) / 1_000_000;
System.out.println("Synchronized method: " + syncTime + "ms");
System.out.println("Double-checked: " + doubleCheckedTime + "ms");
System.out.println("Speedup: " + (double) syncTime / doubleCheckedTime + "x");
}
}
// Результаты (примерные):
// Synchronized method: 8000ms
// Double-checked: 100ms
// Speedup: 80x
Современные альтернативы
1. Enum Singleton (рекомендуется)
public enum Logger {
INSTANCE;
Logger() {
System.out.println("Logger initialized");
// инициализация
}
public void log(String message) {
System.out.println(message);
}
}
// Использование
public class Main {
public static void main(String[] args) {
Logger.INSTANCE.log("Hello");
}
}
Преимущества:
- Потокобезопасно из коробки
- Сериализация работает правильно
- Отражение не может нарушить Singleton
- Никакого boilerplate кода
2. Holder Pattern
public class Logger {
private Logger() {}
// Инициализируется только при первом доступе
private static class LoggerHolder {
static final Logger INSTANCE = new Logger();
}
public static Logger getInstance() {
return LoggerHolder.INSTANCE;
}
}
3. Spring (@Bean с Singleton scope)
@Configuration
public class AppConfig {
@Bean
public Logger logger() {
return new Logger();
}
}
Когда использовать Double-checked Locking
- Ленивая инициализация критична — экономия ресурсов при старте
- Высоконагруженные системы — тысячи вызовов getInstance()
- Кэширование дорогостоящих объектов — DB connections, thread pools
- Legacy code без фреймворков — Spring, Guice не доступны
Когда НЕ использовать
- В Spring приложениях — используй @Singleton бины
- Когда ранняя инициализация ОК — просто создай static field
- Простые Singleton — используй Enum
- Для классов с потокобезопасными операциями — может быть избыточно
Заключение
Double-checked locking — это оптимизация производительности для ленивой инициализации в многопоточной среде. Ключевые моменты:
- Две проверки: без lock (быстро) и с lock (безопасно)
- volatile ОБЯЗАТЕЛЕН для правильной работы
- Производительность улучшается в 10-100 раз по сравнению с полной синхронизацией
- Современные альтернативы лучше: Enum, Holder Pattern, Spring
- Используй только если знаешь, зачем тебе это нужно
В 99% случаев в современной Java разработке существуют лучшие решения. Но понимание double-checked locking глубоко показывает твое знание многопоточности и Java memory model.