BackendBit

К списку паттернов

Singleton – класс, у которого может быть только один экземпляр. Паттерн предоставляет глобальную точку доступа к этому экземпляру – обычно через статический метод getInstance, который возвращает существующий экземпляр или создаёт новый и сохраняет его в приватную переменную.

UML диаграмма классов паттерна ОдиночкаПоказывает класс Singleton с приватным конструктором, статическим полем instance и методом getInstance().Singleton-instance: Singleton+getInstance(): Singleton-Singleton()Приватный конструкторзапрещает созданиечерез new

Наивная реализация

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()); // true
java

Этот пример не потокобезопасен: два потока могут одновременно войти в блок 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;
    }
}
java

JVM гарантирует, что класс 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 означает DefaultServeMux
go

Как и в 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, автоматически прокидывался нужный экземпляр.

Подводя итог, мы советуем не использовать паттерн синглтон без веской причины. В подавляющем большинстве случаев его можно заменить инъекцией зависимостей.