← Назад к вопросам

Что такое шаблон проектирования Singleton?

1.0 Junior🔥 201 комментариев
#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Шаблон проектирования Singleton

Singleton — это один из самых популярных и широко используемых паттернов проектирования. Это паттерн, который ограничивает создание класса только одним единственным экземпляром, при этом предоставляя глобальную точку доступа к этому экземпляру.

Определение Singleton

Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальный метод для получения этого экземпляра. Это полезно для классов, которые должны иметь единственный экземпляр (логирование, конфигурация, подключение к БД).

Классическая реализация (не потокобезопасная)

public class Logger {
    private static Logger instance;
    
    // Приватный конструктор предотвращает создание новых экземпляров
    private Logger() {}
    
    // Глобальный метод доступа к экземпляру
    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
    
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

// Использование
public class Application {
    public static void main(String[] args) {
        Logger logger1 = Logger.getInstance();
        Logger logger2 = Logger.getInstance();
        
        logger1.log("First message");
        logger2.log("Second message");
        
        // logger1 и logger2 — это ОДИН И ТОТ ЖЕ объект
        System.out.println(logger1 == logger2);  // true
    }
}

Проблема: эта реализация НЕ потокобезопасна. В многопоточной среде может создаться несколько экземпляров.

Потокобезопасная реализация 1: synchronized

public class Logger {
    private static Logger instance;
    
    private Logger() {}
    
    // synchronized гарантирует атомарность
    public static synchronized Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
    
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

Минус: synchronized замораживает весь метод, что влияет на производительность.

Потокобезопасная реализация 2: Double-Checked Locking

public class Logger {
    // volatile гарантирует видимость изменений между потоками
    private static volatile Logger instance;
    
    private Logger() {}
    
    public static Logger getInstance() {
        if (instance == null) {              // Первая проверка (без блокировки)
            synchronized (Logger.class) {    // Блокируем только при создании
                if (instance == null) {      // Вторая проверка (с блокировкой)
                    instance = new Logger();
                }
            }
        }
        return instance;
    }
    
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

Преимущество: блокировка происходит только при создании объекта, последующие вызовы быстры.

Потокобезопасная реализация 3: Eager Initialization

public class Logger {
    // Создаем экземпляр при загрузке класса (потокобезопасно)
    private static final Logger instance = new Logger();
    
    private Logger() {}
    
    public static Logger getInstance() {
        return instance;
    }
    
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

Минус: класс создается даже если он не используется.

Потокобезопасная реализация 4: Lazy Initialization с Holder (ЛУЧШИЙ ВАРИАНТ)

public class Logger {
    private Logger() {}
    
    // Holder класс загружается только при первом обращении
    private static class LoggerHolder {
        static final Logger instance = new Logger();
    }
    
    public static Logger getInstance() {
        return LoggerHolder.instance;
    }
    
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

Преимущества:

  • Потокобезопасно (гарантируется ClassLoader)
  • Ленивая инициализация (создается при первом использовании)
  • Высокая производительность
  • Простой и элегантный код

Реализация 5: Enum (РЕКОМЕНДУЕМЫЙ ВАРИАНТ)

public enum Logger {
    INSTANCE;  // Единственный экземпляр
    
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

// Использование
public class Application {
    public static void main(String[] args) {
        Logger.INSTANCE.log("Hello!");
        
        // Гарантирует только один экземпляр
        Logger logger1 = Logger.INSTANCE;
        Logger logger2 = Logger.INSTANCE;
        System.out.println(logger1 == logger2);  // true
    }
}

Преимущества Enum Singleton:

  • Потокобезопасен по умолчанию
  • Защищен от рефлексии
  • Сериализация работает правильно
  • Код краткий и понятный
  • Лучший выбор в современной Java

Практические примеры использования Singleton

1. Логирование

public enum Logger {
    INSTANCE;
    
    private final List<String> logs = new ArrayList<>();
    
    public void log(String message) {
        logs.add(message);
        System.out.println("[LOG] " + message);
    }
    
    public List<String> getLogs() {
        return new ArrayList<>(logs);
    }
}

2. Конфигурация

public enum AppConfig {
    INSTANCE;
    
    private final Properties properties = new Properties();
    
    AppConfig() {
        // Загрузить конфигурацию
    }
    
    public String getProperty(String key) {
        return properties.getProperty(key);
    }
}

3. Подключение к базе данных

public enum DatabaseConnection {
    INSTANCE;
    
    private Connection connection;
    
    DatabaseConnection() {
        try {
            this.connection = DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/mydb",
                "user",
                "password"
            );
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    public Connection getConnection() {
        return connection;
    }
}

Когда использовать Singleton

  • Логирование (Logger)
  • Конфигурация приложения (AppConfig)
  • Пул соединений с БД
  • Кэш данных
  • Управление ресурсами
  • Фасад к подсистеме

Когда НЕ использовать Singleton

  • Если нужно несколько экземпляров
  • Если нужно легко подменять реализацию для тестирования
  • Если нужна гибкость в создании объектов

Проблемы Singleton

  1. Тестирование — сложно mock-ировать
  2. Скрытые зависимости — глобальное состояние
  3. Многопоточность — требует внимательной реализации
  4. Нарушение принципа Single Responsibility — объект отвечает и за создание, и за функциональность

Решение проблем: Dependency Injection

@Service
public class UserService {
    private final Logger logger;
    
    // Инъекция зависимости вместо Singleton
    public UserService(Logger logger) {
        this.logger = logger;
    }
    
    public void createUser(String name) {
        logger.log("Creating user: " + name);
    }
}

Вывод

Singleton — это полезный паттерн для случаев, когда нужен ровно один экземпляр класса. Современный и рекомендуемый способ реализации — это Enum. Однако в больших проектах предпочтительно использовать Dependency Injection (Spring, Dagger и т.д.) вместо Singleton паттерна, так как это делает код более тестируемым и гибким.