🍃 Раздел 5 · Вопрос #12

В чём разница между singleton и prototype scope

Singleton и Prototype — две базовые области видимости бинов в Spring. Разница между ними заключается в количестве создаваемых объектов, моменте их создания и управлении их жизне...


🟢 Junior Level

Singleton и Prototype — две базовые области видимости бинов в Spring. Разница между ними заключается в количестве создаваемых объектов, моменте их создания и управлении их жизненным циклом.

Главные отличия

Характеристика Singleton (по умолчанию) Prototype
Количество объектов Один экземпляр на весь ApplicationContext Новый экземпляр при каждом обращении/инъекции
Момент создания При старте приложения (Eager, если не @Lazy) По требованию в рантайме (On Demand / Lazy)
Где хранится ссылка В кэше синглтонов Spring (singletonObjects) Не хранится контейнером после отдачи клиенту
Вызов @PostConstruct Вызывается один раз при старте Вызывается каждый раз для нового объекта
Вызов @PreDestroy ✅ Вызывается при закрытии контекста ❌ НЕ вызывается контейнером!
Потокобезопасность Должен быть Stateless (без состояния) Может быть Stateful (хранить состояние)

Базовый пример

package com.example.demo;

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

@Component
public class CacheManager {
    // Singleton: один общий объект для всех потоков
}

@Component
@Scope("prototype")
public class DataImportJob {
    // Prototype: каждый импорт данных создаёт изолированный объект
    private String fileName;

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
}
// Проверка равенства ссылок:
CacheManager c1 = context.getBean(CacheManager.class);
CacheManager c2 = context.getBean(CacheManager.class);
System.out.println(c1 == c2); // true (один и тот же объект в памяти)

DataImportJob j1 = context.getBean(DataImportJob.class);
DataImportJob j2 = context.getBean(DataImportJob.class);
System.out.println(j1 == j2); // false (два разных объекта в Heap)

🟡 Middle Level

Различия в жизненном цикле (Lifecycle)

Жизненные циклы singleton и prototype совпадают только на этапе создания, но кардинально расходятся на этапе уничтожения:

Фаза                     Singleton                 Prototype
----------------------------------------------------------------------
1. Instantiation         Вызывается (1 раз)        Вызывается (N раз)
2. Populate Properties   Вызывается (1 раз)        Вызывается (N раз)
3. Aware Interfaces      Вызывается (1 раз)        Вызывается (N раз)
4. BPP BeforeInit        Вызывается (1 раз)        Вызывается (N раз)
5. @PostConstruct        Вызывается (1 раз)        Вызывается (N раз)
6. BPP AfterInit (Proxy) Вызывается (1 раз)        Вызывается (N раз)
----------------------------------------------------------------------
7. Удержание ссылки      В кэше контейнера         Отдается клиенту (GC-eligible)
----------------------------------------------------------------------
8. @PreDestroy           ✅ Вызывается контейнером  ❌ НЕ вызывается контейнером
9. DisposableBean        ✅ Вызывается контейнером  ❌ НЕ вызывается контейнером
10. destroy-method       ✅ Вызывается контейнером  ❌ НЕ вызывается контейнером

Проблема очистки ресурсов в Prototype

Поскольку контейнер не отслеживает деструкцию prototype-бинов, освобождать ресурсы обязан клиентский код:

package com.example.demo;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
public class ReportOrchestrator {

    private final ObjectProvider<PdfExportTask> taskProvider;

    public ReportOrchestrator(ObjectProvider<PdfExportTask> taskProvider) {
        this.taskProvider = taskProvider;
    }

    public void generate() {
        PdfExportTask task = taskProvider.getObject();
        try {
            task.execute();
        } finally {
            // Ручное освобождение ресурсов! Иначе файлы и сокеты останутся открытыми
            task.cleanup(); 
        }
    }
}

Внедрение Prototype в Singleton: ловушка «заморозки»

Если заинжектить prototype-бин напрямую в singleton:

@Service // Singleton
public class OrderService {

    @Autowired // ВНИМАНИЕ: Prototype внедрится всего ОДИН раз при старте сервиса!
    private PaymentValidator validator; 
}

Результат: PaymentValidator создается один раз при сборке OrderService и навсегда застревает в нем. Чтобы при каждом вызове получать свежий экземпляр, используют ObjectProvider<PaymentValidator> или методную инъекцию @Lookup.


🔴 Senior Level

Циклические зависимости: фундаментальное различие

  • Для Singleton: Контейнер умеет разрешать циклические зависимости (при использовании setter/field injection) благодаря трёхуровневому кэшу (singletonObjects, earlySingletonObjects, singletonFactories). Фабрика ранней ссылки публикует объект в куче до его полной инициализации.
  • Для Prototype: Циклические зависимости неразрешимы в принципе. Если бин ProtoA зависит от ProtoB, а ProtoB зависит от ProtoA:
    1. Создается ProtoA, ему требуется ProtoB.
    2. Создается новый экземпляр ProtoB, ему требуется новый экземпляр ProtoA.
    3. Создается новый экземпляр ProtoA…
    4. Процесс уходит в бесконечную рекурсию. Spring отслеживает стек вызовов и немедленно выбрасывает: BeanCurrentlyInCreationException: Error creating bean with name 'protoA': Requested bean is currently in creation: Is there an unresolvable circular reference?

Нагрузка на производительность и GC Overhead

Использование Prototype-бинов в критических по производительности путях (High-Throughput Hotpaths) — частая ошибка архитекторов.

Сравнение стоимости:

  1. Singleton:
    • Затраты на рефлексию, поиск аннотаций, внедрение зависимостей и генерацию прокси происходят один раз при прогреве приложения.
    • В рантайме вызов метода бина — это прямой вызов ссылки со скоростью процессора (JIT inlining).
  2. Prototype:
    • При каждом запросе getBean() контейнер выполняет:
      1. Рефлексивный вызов конструктора (Constructor.newInstance).
      2. Полный прогон через цепочку BeanPostProcessor (postProcessProperties, инъекция всех полей).
      3. Вызов @PostConstruct и Aware-методов.
      4. Потенциальную генерацию/оборачивание в AOP-прокси (ProxyFactory).
    • Это создает миллионы короткоживущих объектов в секунду, приводя к высокой частоте вызовов Young GC (GC Pauses) и резкому росту нагрузки на CPU.

Когда действительно нужен Prototype, а когда обычный new?

Нужен ли новый экземпляр?
       │
       ├── Нет → Singleton (Stateless)
       │
       └── Да → Нужны ли внутри объекта зависимости Spring (@Autowired, @Transactional, @Value)?
                  │
                  ├── Нет → Используйте обычный `new MyObject()`! (В 100 раз быстрее Prototype)
                  │
                  └── Да → Prototype через ObjectProvider<T>

Правило Senior: Если классу не нужны зависимости от Spring-контейнера, создание его как @Scope("prototype") является антипаттерном. Обычный оператор new в Java не создает оверхеда контейнера и позволяет сборщику мусора применить скалярную замену (Escape Analysis / Scalar Replacement).


🎯 Шпаргалка для интервью

30-секундный ответ

Основные различия Singleton и Prototype:

  1. Количество: Singleton — один экземпляр на ApplicationContext, Prototype — новый объект при каждом запросе.
  2. Управление жизненным циклом: Для Singleton Spring управляет полным циклом, включая уничтожение (@PreDestroy). Для Prototype Spring выполняет только создание и инициализацию, а фазу уничтожения полностью игнорирует.
  3. Хранение в памяти: Singleton удерживается в кэше singletonObjects; Prototype не сохраняется контейнером.
  4. Циклические зависимости: Разрешимы для Singleton через 3-уровневый кэш, но приводят к BeanCurrentlyInCreationException для Prototype.

4 каверзных вопроса с ответами

  1. Почему Spring не вызывает @PreDestroy для бинов со scope prototype? Ответ: Spring не хранит ссылок на созданные prototype-бины. Хранение ссылок на все созданные prototype-экземпляры привело бы к катастрофической утечке памяти, так как сборщик мусора не смог бы их удалить. Поэтому контейнер передает экземпляр клиенту и снимает с себя ответственность за его утилизацию.

  2. Что произойдет, если два Prototype-бина имеют циклическую зависимость друг от друга? Ответ: Приложение выбросит BeanCurrentlyInCreationException. 3-уровневый кэш работает только для синглтонов. Для прототипов нет механизма ранней публикации экземпляра, поэтому контейнер попадает в бесконечную взаимную рекурсию инстанцирования.

  3. Почему прямое внедрение Prototype в Singleton не работает должным образом? Ответ: Singleton-бин создается и наполняется зависимостями один раз при старте приложения. Внедренный prototype-бин закрепляется в поле синглтона и перестает обновляться («замораживается»). Решение: использовать ObjectProvider<T>.getObject() или @Lookup.

  4. Что эффективнее: Prototype-бин или создание объекта через оператор new? Ответ: Оператор new несоизмеримо быстрее и эффективнее. Запрос Prototype-бина запускает тяжелый конвейер контейнера: сканирование метаданных, рефлексию, внедрение зависимостей, прогон через все BeanPostProcessor и возможное создание CGLIB/JDK-прокси. Prototype оправдан только тогда, когда создаваемому объекту требуются внедряемые зависимости Spring.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «@PreDestroy вызывается для Prototype перед тем, как сборщик мусора удалит объект» — грубейшая ошибка. Garbage Collector ничего не знает об аннотациях Spring, а контейнер ссылки не хранит.
  • ❌ «Циклические зависимости в Prototype можно разрешить аннотацией @Lazy» — нет, взаимные циклы прототипов неразрешимы.
  • ❌ «Prototype бины всегда работают быстрее Singleton, потому что не блокируются потоками» — создание Prototype сопряжено с колоссальным оверхедом BPP и рефлексии.
  • ❌ «Singleton бин безопасен для многопоточного изменения полей» — Singleton используется всеми потоками одновременно и обязан быть потокобезопасным (stateless).

Связанные темы