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

Как реализована Lazy Initialization?

2.0 Middle🔥 161 комментариев
#SOLID и паттерны проектирования

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

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

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

# Lazy Initialization

Определение

Lazy Initialization (ленивая инициализация) — это паттерн проектирования, при котором дорогостоящий объект создаётся не при инициализации, а только когда он действительно понадобится (в момент первого обращения).

Основная идея:

Обычно:        создание → хранение → использование
Lazy init:      хранение → использование + создание (при первом обращении)

Пример проблемы

// ПЛОХО: создаём ресурс сразу, даже если он может не понадобиться
public class DatabaseConnection {
    private Connection connection;
    
    public DatabaseConnection() {
        // Дорогостоящая операция подключения
        this.connection = DriverManager.getConnection(
            "jdbc:postgresql://localhost/mydb", "user", "password");
        System.out.println("Connection created!");
    }
    
    public void query(String sql) {
        // Используем connection
    }
}

// Использование
DatabaseConnection db = new DatabaseConnection();
// Соединение уже создано, даже если мы никогда не будем его использовать!
// Это расходует память и других ресурсы

if (someCondition) {
    db.query("SELECT * FROM users");
}

Решение 1: Простой Lazy Initialization

public class DatabaseConnection {
    private Connection connection = null; // null изначально
    
    public void query(String sql) throws SQLException {
        // Создаём соединение при первом использовании
        if (connection == null) {
            connection = DriverManager.getConnection(
                "jdbc:postgresql://localhost/mydb", "user", "password");
            System.out.println("Connection created!");
        }
        
        Statement stmt = connection.createStatement();
        stmt.executeQuery(sql);
    }
}

// Использование
DatabaseConnection db = new DatabaseConnection();
// Соединение ещё не создано

db.query("SELECT * FROM users");
// Соединение создаётся только здесь, при первом обращении

Решение 2: Thread-safe Lazy Initialization

В многопоточной среде нужно быть осторожнее:

Вариант 1: Synchronized метод

public class ThreadSafeDatabaseConnection {
    private Connection connection;
    
    public synchronized Connection getConnection() throws SQLException {
        if (connection == null) {
            connection = DriverManager.getConnection(
                "jdbc:postgresql://localhost/mydb", "user", "password");
            System.out.println("Connection created!");
        }
        return connection;
    }
    
    public void query(String sql) throws SQLException {
        Connection conn = getConnection();
        Statement stmt = conn.createStatement();
        stmt.executeQuery(sql);
    }
}

Минусы:

  • Synchronized на весь метод может быть медленным
  • Даже после инициализации все потоки ждут lock

Вариант 2: Double-Checked Locking (оптимизация)

public class OptimizedDatabaseConnection {
    private volatile Connection connection; // volatile!
    
    public Connection getConnection() throws SQLException {
        // Первая проверка без lock (быстро)
        if (connection == null) {
            synchronized (this) {
                // Вторая проверка с lock (безопасно)
                if (connection == null) {
                    connection = DriverManager.getConnection(
                        "jdbc:postgresql://localhost/mydb", "user", "password");
                    System.out.println("Connection created!");
                }
            }
        }
        return connection;
    }
}

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

  1. Первая проверка без lock (O(1))
  2. Если null, входим в synchronized
  3. Вторая проверка уже с lock (безопасно)
  4. Создаём объект если ещё не создан

volatile гарантирует visibility для всех потоков.

Вариант 3: Holder Pattern (самый чистый)

public class HolderDatabaseConnection {
    private HolderDatabaseConnection() {}
    
    // Вложенный класс для хранения соединения
    private static class ConnectionHolder {
        private static Connection connection;
        
        static {
            try {
                connection = DriverManager.getConnection(
                    "jdbc:postgresql://localhost/mydb", "user", "password");
                System.out.println("Connection created!");
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    public static Connection getConnection() {
        return ConnectionHolder.connection;
    }
}

// Использование
Connection conn = HolderDatabaseConnection.getConnection();
// Соединение создаётся только при первом обращении

Преимущества:

  • Классы загружаются JVM при первом обращении
  • Thread-safe по умолчанию (JVM гарантирует)
  • Никакой явной синхронизации
  • Очень быстро

Решение 3: Java 8 Supplier Pattern

public class SupplierLazyInit<T> {
    private T value;
    private Supplier<T> supplier;
    
    public SupplierLazyInit(Supplier<T> supplier) {
        this.supplier = supplier;
    }
    
    public T get() {
        if (value == null) {
            synchronized (this) {
                if (value == null) {
                    value = supplier.get();
                }
            }
        }
        return value;
    }
}

// Использование
SupplierLazyInit<Connection> connLazy = new SupplierLazyInit<>(() -> {
    try {
        return DriverManager.getConnection(
            "jdbc:postgresql://localhost/mydb", "user", "password");
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
});

// Использование
Connection conn = connLazy.get(); // Создаётся при первом get()

Решение 4: Java 10+ Optional Lazy Init

public class OptionalLazyInit<T> {
    private Optional<T> value = Optional.empty();
    private Supplier<T> supplier;
    
    public OptionalLazyInit(Supplier<T> supplier) {
        this.supplier = supplier;
    }
    
    public T get() {
        if (!value.isPresent()) {
            synchronized (this) {
                if (!value.isPresent()) {
                    value = Optional.of(supplier.get());
                }
            }
        }
        return value.get();
    }
}

Реальный пример: Singleton с Lazy Init

// Плохо: не thread-safe
public class DatabaseSingleton1 {
    private static DatabaseSingleton1 instance;
    
    private DatabaseSingleton1() {}
    
    public static DatabaseSingleton1 getInstance() {
        if (instance == null) { // RACE CONDITION!
            instance = new DatabaseSingleton1();
        }
        return instance;
    }
}

// Хорошо: thread-safe с double-checked locking
public class DatabaseSingleton2 {
    private static volatile DatabaseSingleton2 instance;
    
    private DatabaseSingleton2() {}
    
    public static DatabaseSingleton2 getInstance() {
        if (instance == null) {
            synchronized (DatabaseSingleton2.class) {
                if (instance == null) {
                    instance = new DatabaseSingleton2();
                }
            }
        }
        return instance;
    }
}

// Лучше: используй enum (thread-safe по умолчанию)
public enum DatabaseSingleton3 {
    INSTANCE;
    
    private Connection connection;
    
    DatabaseSingleton3() {
        try {
            this.connection = DriverManager.getConnection(
                "jdbc:postgresql://localhost/mydb", "user", "password");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    
    public Connection getConnection() {
        return connection;
    }
}

// Использование
Connection conn = DatabaseSingleton3.INSTANCE.getConnection();

Spring Framework: Lazy Initialization

В Spring бины по умолчанию инициализируются eager, но можно сделать lazy:

@Configuration
public class AppConfig {
    @Bean
    @Lazy // Инициализируется при первом обращении
    public DatabaseConnection databaseConnection() {
        return new DatabaseConnection();
    }
}

@Component
@Lazy
public class ExpensiveService {
    // Создаётся только когда впервые затребуется
}

// Использование
@Autowired
private ObjectProvider<ExpensiveService> serviceProvider; // ObjectProvider поддерживает Lazy

public void doSomething() {
    ExpensiveService service = serviceProvider.getIfAvailable();
    // service инициализируется только здесь
}

Практический пример: HTTP Client

public class ApiClient {
    private HttpClient httpClient;
    
    // Ленивая инициализация HTTP клиента
    public HttpClient getHttpClient() {
        if (httpClient == null) {
            synchronized (this) {
                if (httpClient == null) {
                    httpClient = HttpClient.newBuilder()
                        .connectTimeout(Duration.ofSeconds(10))
                        .build();
                    System.out.println("HttpClient created");
                }
            }
        }
        return httpClient;
    }
    
    public String fetchData(String url) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
        HttpResponse<String> response = getHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        return response.body();
    }
}

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

Используй, когда:

  • Объект дорогостоящий (DB соединение, HTTP клиент)
  • Объект может не понадобиться
  • Время инициализации критично
  • Есть много таких объектов

Не используй, когда:

  • Объект дешёвый для создания
  • Объект всегда нужен
  • Нужна простота кода
  • Гарантированно используется при запуске

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

long start = System.currentTimeMillis();
DatabaseConnection db = new DatabaseConnection();
long elapsed = System.currentTimeMillis() - start;

// Без lazy: 500ms (соединение создано)
// С lazy: 1ms (ничего не создано)

db.query("SELECT ...");
// Первый запрос: 500ms + время запроса
// Последующие: время запроса

Заключение

Lazy Initialization — это паттерн для отложенного создания дорогостоящих объектов. Основные подходы:

  1. Простой — проверка null в getter
  2. Thread-safe — synchronized + volatile
  3. Оптимизированный — double-checked locking
  4. Идеальный — Holder pattern или enum singleton
  5. Фреймворки — Spring @Lazy, ObjectProvider

Выбирай подход в зависимости от требований к производительности и потокобезопасности.