
Привет, Хабр!
Паттерн Singleton гарантирует существование лишь одного экземпляра класса и предоставляет к нему глобальную точку доступа. Этот паттерн стал почти синонимом чистоты кода в многих сценариях работы с Java, где требуется строго один экземпляр объекта. Но не менее интересный и гибкий паттерн - это Multiton. Менее известный, но не менее мощный, он позволяет создавать множество экземпляров класса и контролировать их число и жизненный цикл через предопределенные ключи.
В этой статье мы рассмотрим эти паттерны и их различия.
Singleton
Суть Singleton заключается не просто в ограничении инстанцирования класса одним объектом, но и в предоставлении универсальной точки доступа к этому экземпляру.
Пн помогает контролировать доступ к ресурсам, которые по своей природе должны быть уникальными. Т.е: доступ к БД, файловой системе или какому-либо общему ресурсу, который требует строгого контроля над своим состоянием и доступностью.
Singleton работает по принципу отложенной инициализации — экземпляр класса создается только тогда, когда он впервые нужен. Т.е так ресурсы используются экономно и эффективно, поскольку инициализация происходит только по требованию.
С точки зрения реализации, основными моментами Singleton являются:
Приватный конструктор, который предотвращает прямое создание объекта класса.
Статическая переменная, которая хранит экземпляр Singleton класса.
Публичный статический метод, который предоставляет глобальный доступ к этому экземпляру. Если экземпляр еще не создан, метод его инициализирует; если уже создан - возвращает ссылку на существующий.
Тем не менее, использование Singleton не всегда оправдано. В частности, его применение может затруднить тестирование кода из-за сложностей с мокированием зависимостей и может привести к нежелательной связанности компонентов системы. Также в многопоточных средах требуется доп. осторожность, чтобы обеспечить потокобезопасность Singleton, что часто достигается за счет синхронизации, что, в свою очередь, может негативно сказаться на производительности.
Примеры кода
Рассмотрим пять классических вариантов:
Самый базовый вариант Singleton включает в себя приватный конструктор и статический метод для получения экземпляра:
public class ClassicSingleton { private static ClassicSingleton instance; private ClassicSingleton() {} public static ClassicSingleton getInstance() { if (instance == null) { instance = new ClassicSingleton(); } return instance; } }
Для обеспечения потокобезопасности в многопоточной среде используется синхронизация:
public class ThreadSafeSingleton { private static ThreadSafeSingleton instance; private ThreadSafeSingleton() {} public static synchronized ThreadSafeSingleton getInstance() { if (instance == null) { instance = new ThreadSafeSingleton(); } return instance; } }
Lazy Holder использует вложенный статический класс для отложенной инициализации экземпляра:
public class LazyHolderSingleton { private LazyHolderSingleton() {} private static class LazyHolder { static final LazyHolderSingleton INSTANCE = new LazyHolderSingleton(); } public static LazyHolderSingleton getInstance() { return LazyHolder.INSTANCE; } }
Использование перечислений для реализации Singleton гарантирует противодействие проблемам сериализации:
public enum EnumSingleton { INSTANCE; public void someMethod() { // Реализация метода System.out.println("Log message: " + message); } }
Double-checked locking для ленивой инициализации уменьшает затраты на синхронизацию, проверяя экземпляр дважды:
public class DoubleCheckedLockingSingleton { private static volatile DoubleCheckedLockingSingleton instance; private DoubleCheckedLockingSingleton() {} public static DoubleCheckedLockingSingleton getInstance() { if (instance == null) { synchronized (DoubleCheckedLockingSingleton.class) { if (instance == null) { instance = new DoubleCheckedLockingSingleton(); } } } return instance; } }
Реализуем логирование и управление подключением к БД
Системы логирования - это классический пример использования Singleton, поскольку обычно требуется единственный экземпляр логгера на всё приложение. Т.е с паттерном все части приложения используют один и тот же экземпляр логгера:
public class LoggerSingleton { private static LoggerSingleton instance; private LoggerSingleton() {} public static synchronized LoggerSingleton getInstance() { if (instance == null) { instance = new LoggerSingleton(); } return instance; } public void log(String message) { // примитивная реализация записи сообщения в лог System.out.println(System.currentTimeMillis() + ": " + message); } }
Singleton также часто используется для управления подключениями к БД, гарантируя, что вся система использует единственное подключение, или управляет пулом подключений через олин экземпляр:
public class DatabaseConnectionSingleton { private static DatabaseConnectionSingleton instance; private Connection connection; private DatabaseConnectionSingleton() { try { // инициализация подключения к БД this.connection = DriverManager.getConnection("jdbc:example:database:url", "user", "password"); } catch (SQLException e) { // обработка исключения } } public static DatabaseConnectionSingleton getInstance() { if (instance == null) { synchronized (DatabaseConnectionSingleton.class) { if (instance == null) { instance = new DatabaseConnectionSingleton(); } } } return instance; } public Connection getConnection() { return connection; } }
Multiton
Multiton это порождающий паттерн проектирования, который обеспечивает контролируемое создание объектов класса с использованием ассоциативного массива для хранения и доступа к экземплярам по уникальным ключам.
В основе паттерна лежит идея о том, что для некоторых классов может потребоваться не один, а несколько экземпляров, каждый из которых связан с определенным ключом. Это позволяет избегать глобального состояния, связанного с Singleton.
Примеры
Пул подключения к БД:
public class DatabaseConnection { private static final Map<String, DatabaseConnection> instances = new HashMap<>(); private DatabaseConnection() { // инициализация подключения к базе данных } public static synchronized DatabaseConnection getInstance(String dbName) { if (!instances.containsKey(dbName)) { instances.put(dbName, new DatabaseConnection()); } return instances.get(dbName); } }
Кэширование объекта:
public class ObjectCache { private static final Map<String, Object> cache = new HashMap<>(); private ObjectCache() { // инициализация кэша } public static synchronized Object getInstance(String key) { if (!cache.containsKey(key)) { cache.put(key, new Object()); } return cache.get(key); } }
Лог разных модулей приложения:
public class Logger { private static final Map<String, Logger> loggers = new HashMap<>(); private String moduleName; private Logger(String moduleName) { this.moduleName = moduleName; // инициализация логгера } public static synchronized Logger getInstance(String moduleName) { if (!loggers.containsKey(moduleName)) { loggers.put(moduleName, new Logger(moduleName)); } return loggers.get(moduleName); } public void log(String message) { System.out.println("[" + moduleName + "] " + message); }
Сравним эти паттерны
Singleton:
Описание: гарантирует, что для класса существует только один экземпляр, и предоставляет глобальную точку доступа к нему.
Управление экземпляром: один экземпляр класса.
Ограничение: нет возможности создавать несколько экземпляров класса.
Идентификация: единственный экземпляр идентифицируется по статическому методу или переменной.
Применение: используется для доступа к общим ресурсам, кэширования объектов, логирования и т.д.
Multiton:
Описание: похож на Singleton, но позволяет создавать и управлять множеством экземпляров класса с уникальными ключами.
Управление экземпляром: множество экземпляров класса, каждый из которых идентифицируется уникальным ключом.
Ограничение: ограниченное кол-воэкземпляров, определенных по ключам.
Идентификация: каждый экземпляр идентифицируется уникальным ключом.
Применение: спользуется для управления пулами ресурсов, кэширования с ограниченным размером, управления соединениями к БД и т.д.
Для наглядности сделал табличку:
Параметр | Singleton | Multiton |
|---|---|---|
Управление | Один экземпляр класса | Множество экземпляров по ключам |
Ограничение | Один экземпляр | Ограниченное количество |
Идентификация | По статическому методу или переменной | По уникальному ключу |
Применение | Общие ресурсы, кэширование, логирование | Пулы ресурсов, кэширование с ограничением, управление соединениями к БД и т.д. |
Итак, если задача требует существования только одного экземпляра класса в приложении, например, для доступа к общим ресурсам или управления глобальными настройками, то Singleton - хороший выбор. Если нужно иметь несколько экземпляров класса с различными характеристиками или параметрами, например, для управления пулами ресурсов с ограниченным размером или для работы с разными источниками данных, то Multiton конечно будет намного лучше.
В завершение хочу пригласить вас на бесплатный вебинар, где вы узнаете, что такое дамп памяти, как его собрать и какие инструменты существуют для этих целей. Далее вы познакомитесь с инструментом Eclipse Memory Analyzer, с помощью которого можно исследовать дампы памяти, особенно, если у вас возникает OutOfMemory.

