Привет, Хабр!
Паттерн 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.