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

Что такое ленивая инициализация?

1.8 Middle🔥 151 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью

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

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

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

Ленивая инициализация (Lazy Initialization)

Ленивая инициализация — это паттерн проектирования, при котором объект или ресурс создаются не в момент инициализации класса, а только когда они впервые требуются. Это позволяет улучшить производительность приложения, отложив дорогостоящие операции (создание объектов, открытие соединений с БД, загрузка больших данных) до момента их фактического использования.

Ленивая инициализация особенно полезна в приложениях с ограниченными ресурсами или когда не все объекты используются во время выполнения программы.

Простой пример ленивой инициализации

public class DatabaseConnection {
    
    private Connection connection; // Инициализируется как null
    
    // Геттер с ленивой инициализацией
    public Connection getConnection() {
        if (connection == null) {
            connection = createConnection();
            System.out.println("Соединение с БД создано");
        }
        return connection;
    }
    
    private Connection createConnection() {
        // Имитация создания соединения (дорогостоящая операция)
        try {
            Class.forName("org.postgresql.Driver");
            return DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/mydb",
                "user",
                "password"
            );
        } catch (Exception e) {
            throw new RuntimeException("Ошибка подключения", e);
        }
    }
    
    public static void main(String[] args) {
        DatabaseConnection db = new DatabaseConnection();
        
        // Соединение создаётся только здесь
        Connection conn1 = db.getConnection(); // Выведет: Соединение с БД создано
        Connection conn2 = db.getConnection(); // Выведет: (ничего, уже создано)
    }
}

Потокобезопасная ленивая инициализация

Проблема: простая ленивая инициализация не потокобезопасна — в многопоточном окружении могут возникнуть race conditions.

Решение 1: синхронизированный геттер

public class ThreadSafeLazyInit {
    
    private ExpensiveObject resource;
    
    // Синхронизация обеспечивает потокобезопасность
    public synchronized ExpensiveObject getResource() {
        if (resource == null) {
            resource = new ExpensiveObject();
            System.out.println("Ресурс создан в потоке: " + Thread.currentThread().getName());
        }
        return resource;
    }
    
    static class ExpensiveObject {
        // Дорогостоящий объект
    }
}

Решение 2: Double-Checked Locking (DCL)

Это оптимизация синхронизированного геттера — проверяем переменную дважды:

public class DoubleCheckedLocking {
    
    // volatile гарантирует видимость изменений между потоками
    private volatile ExpensiveObject resource;
    
    public ExpensiveObject getResource() {
        // Первая проверка без синхронизации (быстрая)
        if (resource == null) {
            synchronized(this) {
                // Вторая проверка уже под синхронизацией
                if (resource == null) {
                    resource = new ExpensiveObject();
                    System.out.println("Ресурс создан");
                }
            }
        }
        return resource;
    }
    
    static class ExpensiveObject {
    }
}

Почему volatile? — Без volatile другой поток может не увидеть обновленное значение resource из-за кеша процессора.

Решение 3: Holder Pattern (рекомендуется)

Это самый элегантный и безопасный способ:

public class HolderPattern {
    
    private HolderPattern() {
        // Приватный конструктор предотвращает создание извне
    }
    
    // Внутренний класс инициализируется только когда на него ссылаются
    private static class ResourceHolder {
        static final ExpensiveObject resource = new ExpensiveObject();
        
        static {
            System.out.println("ResourceHolder инициализирован");
        }
    }
    
    public static ExpensiveObject getInstance() {
        return ResourceHolder.resource;
    }
    
    static class ExpensiveObject {
    }
    
    public static void main(String[] args) {
        System.out.println("main запущен");
        // Класс ResourceHolder инициализируется только здесь
        ExpensiveObject obj = getInstance();
    }
}

// Вывод:
// main запущен
// ResourceHolder инициализирован

Решение 4: Java 8+ — Supplier с мемоизацией

import java.util.function.Supplier;

public class LazySupplier {
    
    private final Supplier<ExpensiveObject> lazy;
    
    // Мемоизируем результат Supplier
    public LazySupplier() {
        this.lazy = memoize(() -> {
            System.out.println("Создание ресурса");
            return new ExpensiveObject();
        });
    }
    
    public ExpensiveObject get() {
        return lazy.get();
    }
    
    // Вспомогательный метод для мемоизации
    private static <T> Supplier<T> memoize(Supplier<T> supplier) {
        return new Supplier<T>() {
            private T value;
            private boolean initialized = false;
            
            @Override
            public T get() {
                if (!initialized) {
                    value = supplier.get();
                    initialized = true;
                }
                return value;
            }
        };
    }
    
    static class ExpensiveObject {
    }
    
    public static void main(String[] args) {
        LazySupplier lazy = new LazySupplier();
        System.out.println("lazy создан");
        
        // Ресурс создаётся здесь
        ExpensiveObject obj1 = lazy.get(); // Выведет: Создание ресурса
        ExpensiveObject obj2 = lazy.get(); // Ничего не выведет
    }
}

Практический пример: ленивая загрузка в Spring

import org.springframework.stereotype.Component;

@Component
public class LazyBeanInitialization {
    
    private LargeDataSet largeDataSet;
    
    // Этот метод вызывается только в первый раз
    private LargeDataSet getLargeDataSet() {
        if (largeDataSet == null) {
            largeDataSet = new LargeDataSet();
            System.out.println("Большой набор данных загружен");
        }
        return largeDataSet;
    }
    
    public void processData() {
        LargeDataSet data = getLargeDataSet();
        // Обработка данных
    }
    
    static class LargeDataSet {
        // Имитация большого объёма данных
        private byte[] data = new byte[1024 * 1024 * 100]; // 100 MB
    }
}

Ленивая инициализация с @Lazy в Spring

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
@Lazy // Bean не создаётся до первого использования
public class ExpensiveService {
    
    public ExpensiveService() {
        System.out.println("ExpensiveService создан (ленивая инициализация)");
    }
    
    public void doSomething() {
        System.out.println("Выполнение операции");
    }
}

@Component
public class ServiceConsumer {
    
    @Autowired
    @Lazy // Inject proxy объект
    private ExpensiveService service;
    
    public void useService() {
        service.doSomething();
    }
}

Ленивая инициализация коллекций

import java.util.*;

public class LazyCollectionInit {
    
    private List<String> items;
    
    public List<String> getItems() {
        if (items == null) {
            items = new ArrayList<>();
            loadItemsFromDatabase();
        }
        return items;
    }
    
    private void loadItemsFromDatabase() {
        System.out.println("Загрузка элементов из БД");
        // Имитация загрузки из БД
        items.add("Item 1");
        items.add("Item 2");
    }
    
    // Или с помощью Collections.synchronizedList для потокобезопасности
    private final List<String> threadSafeItems = Collections.synchronizedList(
        new ArrayList<>()
    );
}

Когда использовать ленивую инициализацию

Полезно:

  • Дорогостоящие ресурсы — подключения к БД, сокеты, файлы
  • Большие данные — которые могут не использоваться
  • Внешние сервисы — HTTP клиенты, API соединения
  • Singleton objects — которые нужны не всегда
  • Конфигурация — которая может быть комплексной

Избегайте:

  • Часто используемые объекты — для них лучше инициализировать сразу
  • Критичные для производительности операции — где задержка неприемлема
  • Где предсказуемость важнее производительности — может скрыть проблемы

Преимущества и недостатки

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

  • Улучшение времени запуска приложения
  • Экономия памяти для неиспользуемых объектов
  • Снижение нагрузки на систему при инициализации

Недостатки:

  • Усложнение кода
  • Потенциальные проблемы с потокобезопасностью
  • Может скрывать проблемы, которые появляются только при использовании
  • Непредсказуемое время первого доступа

Ленивая инициализация — мощный инструмент оптимизации, когда применяется правильно и с пониманием её особенностей.