Singleton (Одиночка)
Singleton – класс, у которого может быть только один экземпляр. Паттерн предоставляет глобальную точку доступа к этому экземпляру – обычно через статический метод getInstance, который возвращает существующий экземпляр или создаёт новый и сохраняет его в приватную переменную.
Наивная реализация
package naive;
public final class Singleton {
private static Singleton instance;
private final long createdAt = System.currentTimeMillis();
// Синглтон нельзя создать через new
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public long getCreatedAt() {
return createdAt;
}
}javaПоле createdAt фиксирует момент создания экземпляра. С его помощью легко убедиться, что getInstance() возвращает один и тот же объект:
Singleton a = Singleton.getInstance();
Singleton b = Singleton.getInstance();
System.out.println(a == b); // true
System.out.println(a.getCreatedAt() == b.getCreatedAt()); // truejavaЭтот пример не потокобезопасен: два потока могут одновременно войти в блок if и создать два разных экземпляра.
Потокобезопасная реализация
Распространённое решение – double-checked locking с volatile-полем и synchronized-блоком, но в Java есть более простой и идиоматический способ – статический внутренний класс (Bill Pugh idiom):
package threadsafe;
public final class Singleton {
private Singleton() {
}
private static final class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}javaJVM гарантирует, что класс Holder будет загружен только при первом обращении к getInstance(). Это обеспечивает ленивую инициализацию и потокобезопасность без использования synchronized.
Ещё один потокобезопасный способ – enum. Джошуа Блох в книге «Effective Java» рекомендует именно его: enum автоматически защищён от повторного создания через reflection и сериализацию. Однако у этого подхода есть ограничения: enum не может наследовать от класса и не может принимать рантайм-параметры, что ограничивает его применимость в случаях, когда синглтону нужна конфигурация при инициализации.
Реалистичный пример
Можно представить логгер в виде синглтона.
package logger;
import java.io.PrintStream;
public final class Logger {
private final PrintStream output;
private Logger(PrintStream output) {
this.output = output;
}
private static final class Holder {
private static final Logger INSTANCE = new Logger(System.out);
}
public static Logger getInstance() {
return Holder.INSTANCE;
}
public void log(String message) {
output.println(message);
}
}javaЕго можно использовать в любой точке приложения:
Logger.getInstance().log("something happened");javaРеальные примеры
Runtime.getRuntime()
java.lang.Runtime – класс стандартной библиотеки, через который приложение взаимодействует с JVM: узнаёт объём доступной памяти, количество процессоров, запускает внешние процессы и регистрирует shutdown-хуки. Runtime реализован как синглтон – JVM создаёт единственный экземпляр, а статический метод getRuntime() предоставляет к нему глобальную точку доступа.
Runtime runtime = Runtime.getRuntime();
// Информация о памяти JVM
long maxMemory = runtime.maxMemory();
long freeMemory = runtime.freeMemory();
long totalMemory = runtime.totalMemory();
// Количество доступных процессоров
int processors = runtime.availableProcessors();
// Добавление shutdown hook
runtime.addShutdownHook(new Thread(() -> {
System.out.println("JVM is shutting down");
}));javaЭто оправданное использование синглтона – JVM-процесс один, и несколько экземпляров Runtime не имели бы смысла.
sync.Once
В Go нет классов и статических методов, поэтому паттерн Singleton выглядит иначе. Идиоматический способ гарантировать однократную инициализацию – sync.Once из стандартной библиотеки. Это аналог потокобезопасной ленивой инициализации, которую мы рассмотрели выше в Bill Pugh idiom.
package db
import (
"database/sql"
"sync"
_ "github.com/lib/pq"
)
var (
instance *sql.DB
initErr error
once sync.Once
)
func GetConnection() (*sql.DB, error) {
once.Do(func() {
instance, initErr = sql.Open("postgres", "postgres://localhost/mydb")
if initErr != nil {
return
}
initErr = instance.Ping()
})
return instance, initErr
}goСтандартная библиотека Go использует более простую форму синглтона – переменные уровня пакета. Например, http.DefaultClient и http.DefaultServeMux инициализируются при загрузке пакета (аналог eager initialization):
// http.DefaultClient – синглтон для HTTP-запросов по умолчанию
resp, err := http.Get("https://example.com")
// http.DefaultServeMux – синглтон для маршрутизации
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil) // nil означает DefaultServeMuxgoКак и в Java, в Go для тестируемости предпочтительнее передавать зависимости явно, а не полагаться на пакетные синглтоны.
Почему Singleton считается антипаттерном?
Синглтон даёт простоту использования, и бывают случаи, когда он оправдан – например, для управления доступом к аппаратным ресурсам. Однако в типичном приложении за этой простотой кроются проблемы:
- Синглтон создаёт глобальный стейт. Можно считать, что это как глобальная переменная, которая может быть использована любым модулем программы. Глобальные переменные усложняют контроль за зависимостями и могут затруднить дебаггинг;
- Для классов, которые используют синглтон, тяжело писать юнит-тесты. Зависимость от конкретного класса через статический вызов
Logger.getInstance()делает подмену сложной и требует специальных инструментов (например,Mockito.mockStatic), тогда как при инъекции зависимостей достаточно передать мок через конструктор; - В синглтон-классе смешиваются несколько обязанностей: доменная логика и управление собственным жизненным циклом (создание экземпляра, гарантия уникальности, глобальный доступ). Это два разных вектора изменений, нарушающих принцип единственной ответственности.
Даже Logger из примера выше можно переписать без использования синглтона. В Java для логирования принято использовать фасад логирования SLF4J. Тот же принцип применим к любым зависимостям – вместо статического getInstance() зависимость прокидывается через конструктор:
package reports;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ReportGenerator {
private static final Logger logger = LoggerFactory.getLogger(ReportGenerator.class);
private final ReportRepository repository;
public ReportGenerator(ReportRepository repository) {
this.repository = repository;
}
public void generate() {
logger.info("Starting generating a report...");
// Логика генерации отчёта
}
}javaЛоггер получается через фабричный метод LoggerFactory.getLogger() – библиотека сама управляет экземплярами. Куда писать логи (stdout, файл, сеть) определяется конфигурацией (например, в logback.xml), а не кодом. А ReportRepository прокидывается через конструктор.
В современных фреймворках, таких как Spring, можно использовать DI (Dependency Injection) контейнер. Можно настроить его таким образом, что везде, где в конструкторе объявлен параметр типа ReportRepository, автоматически прокидывался нужный экземпляр.
Подводя итог, мы советуем не использовать паттерн синглтон без веской причины. В подавляющем большинстве случаев его можно заменить инъекцией зависимостей.