В чём разница между 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:- Создается
ProtoA, ему требуетсяProtoB. - Создается новый экземпляр
ProtoB, ему требуется новый экземплярProtoA. - Создается новый экземпляр
ProtoA… - Процесс уходит в бесконечную рекурсию.
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) — частая ошибка архитекторов.
Сравнение стоимости:
- Singleton:
- Затраты на рефлексию, поиск аннотаций, внедрение зависимостей и генерацию прокси происходят один раз при прогреве приложения.
- В рантайме вызов метода бина — это прямой вызов ссылки со скоростью процессора (JIT inlining).
- Prototype:
- При каждом запросе
getBean()контейнер выполняет:- Рефлексивный вызов конструктора (
Constructor.newInstance). - Полный прогон через цепочку
BeanPostProcessor(postProcessProperties, инъекция всех полей). - Вызов
@PostConstructи Aware-методов. - Потенциальную генерацию/оборачивание в 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:
- Количество:
Singleton— один экземпляр наApplicationContext,Prototype— новый объект при каждом запросе.- Управление жизненным циклом: Для
SingletonSpring управляет полным циклом, включая уничтожение (@PreDestroy). ДляPrototypeSpring выполняет только создание и инициализацию, а фазу уничтожения полностью игнорирует.- Хранение в памяти:
Singletonудерживается в кэшеsingletonObjects;Prototypeне сохраняется контейнером.- Циклические зависимости: Разрешимы для
Singletonчерез 3-уровневый кэш, но приводят кBeanCurrentlyInCreationExceptionдляPrototype.
4 каверзных вопроса с ответами
-
Почему Spring не вызывает
@PreDestroyдля бинов со scopeprototype? Ответ: Spring не хранит ссылок на созданные prototype-бины. Хранение ссылок на все созданные prototype-экземпляры привело бы к катастрофической утечке памяти, так как сборщик мусора не смог бы их удалить. Поэтому контейнер передает экземпляр клиенту и снимает с себя ответственность за его утилизацию. -
Что произойдет, если два Prototype-бина имеют циклическую зависимость друг от друга? Ответ: Приложение выбросит
BeanCurrentlyInCreationException. 3-уровневый кэш работает только для синглтонов. Для прототипов нет механизма ранней публикации экземпляра, поэтому контейнер попадает в бесконечную взаимную рекурсию инстанцирования. -
Почему прямое внедрение Prototype в Singleton не работает должным образом? Ответ: Singleton-бин создается и наполняется зависимостями один раз при старте приложения. Внедренный prototype-бин закрепляется в поле синглтона и перестает обновляться («замораживается»). Решение: использовать
ObjectProvider<T>.getObject()или@Lookup. -
Что эффективнее: Prototype-бин или создание объекта через оператор
new? Ответ: Операторnewнесоизмеримо быстрее и эффективнее. Запрос Prototype-бина запускает тяжелый конвейер контейнера: сканирование метаданных, рефлексию, внедрение зависимостей, прогон через всеBeanPostProcessorи возможное создание CGLIB/JDK-прокси. Prototype оправдан только тогда, когда создаваемому объекту требуются внедряемые зависимости Spring.
Красные флаги (чего категорически нельзя говорить)
- ❌ «
@PreDestroyвызывается для Prototype перед тем, как сборщик мусора удалит объект» — грубейшая ошибка. Garbage Collector ничего не знает об аннотациях Spring, а контейнер ссылки не хранит. - ❌ «Циклические зависимости в Prototype можно разрешить аннотацией
@Lazy» — нет, взаимные циклы прототипов неразрешимы. - ❌ «Prototype бины всегда работают быстрее Singleton, потому что не блокируются потоками» — создание Prototype сопряжено с колоссальным оверхедом BPP и рефлексии.
- ❌ «Singleton бин безопасен для многопоточного изменения полей» — Singleton используется всеми потоками одновременно и обязан быть потокобезопасным (stateless).