Одиночка (Singleton)
Singleton ограничивает создание класса одним экземпляром в заданной области и предоставляет точку доступа к нему.
Ключевое уточнение для Java: «один» обычно означает один экземпляр на конкретный ClassLoader. В нескольких JVM, application contexts или class loaders экземпляров может быть несколько.
Когда встречается
- stateless registry;
- локальный cache с одним владельцем;
- holder для неизменяемой конфигурации;
- объект, жизненным циклом которого управляет DI container.
Соединение с БД — плохой пример одиночного ресурса: приложению обычно нужен pool из нескольких connections.
Eager initialization
public final class MetricsRegistry {
private static final MetricsRegistry INSTANCE =
new MetricsRegistry();
private MetricsRegistry() {
}
public static MetricsRegistry instance() {
return INSTANCE;
}
}Инициализация класса в JVM потокобезопасна. Экземпляр будет создан при инициализации MetricsRegistry, даже если позже не понадобится.
Initialization-on-demand holder
public final class MetricsRegistry {
private MetricsRegistry() {
}
private static class Holder {
private static final MetricsRegistry INSTANCE =
new MetricsRegistry();
}
public static MetricsRegistry instance() {
return Holder.INSTANCE;
}
}Вложенный Holder инициализируется при первом обращении к его полю. Получается lazy initialization без ручного synchronized.
Enum Singleton
public enum ApplicationClock {
INSTANCE;
public Instant now() {
return Instant.now();
}
}Enum защищён правилами языка от обычного создания через reflection и корректно восстанавливает singleton-семантику при Java serialization. Но глобальная точка доступа всё равно связывает клиентов с конкретной реализацией.
Почему простой lazy-вариант сломан
if (instance == null) {
instance = new MetricsRegistry();
}
return instance;Два потока могут одновременно увидеть null и создать два объекта. Double-checked locking работает только при volatile:
private static volatile MetricsRegistry instance;
public static MetricsRegistry instance() {
MetricsRegistry result = instance;
if (result == null) {
synchronized (MetricsRegistry.class) {
result = instance;
if (result == null) {
result = new MetricsRegistry();
instance = result;
}
}
}
return result;
}Для обычного приложения holder или enum проще и надёжнее.
Почему Singleton считают проблемным
Само единственное владение ресурсом не является ошибкой. Проблемы создаёт скрытый глобальный доступ:
- зависимость не видна в constructor signature;
- состояние разделяют несвязанные тесты;
- реализацию трудно заменить;
- lifecycle живёт внутри класса вместе с прикладной ответственностью;
- конкурентный доступ к mutable state остаётся отдельной задачей.
DI container позволяет иметь один экземпляр, но передавать его явно как зависимость:
final class ReportService {
private final Clock clock;
ReportService(Clock clock) {
this.clock = clock;
}
}Это упрощает тестирование и не привязывает код к Clock.instance().
Spring singleton scope
Singleton bean в Spring — один bean instance на ApplicationContext и bean name. Это не гарантия одного Java-объекта на всю JVM. Два contexts могут создать по экземпляру.
Вопросы для собеседования
Гарантирует ли private constructor единственный объект?
Нет. Он только ограничивает обычный вызов конструктора. Нужны правила создания; остаются reflection, serialization, cloning и разные class loaders.
Делает ли Singleton объект потокобезопасным?
Нет. Потокобезопасно можно получить ссылку, но mutable поля объекта требуют собственной синхронизации.
Чем singleton bean отличается от GoF Singleton?
Container управляет scope и внедряет зависимость снаружи. Класс необязательно содержит static instance и глобальный accessor.
Когда Singleton оправдан?
Когда область единственности и lifecycle действительно определены, состояние контролируется, а клиенты не получают скрытую зависимость без необходимости.