[Перевод] Синглтоны в Java
В этом кратком руководстве мы рассмотрим два наиболее популярных способа реализации синглтонов в Java.
2. Синглтон на основе класса
Наиболее распространённым подходом является создание синглтона путём создания обычного класса и проверки того, что он имеет:
приватный конструктор;
статическое поле, содержащее его единственный экземпляр;
статический фабричный метод для получения экземпляра.
Мы также добавим свойство info
для последующего использования. Таким образом, наша реализация будет выглядеть следующим образом:
public final class ClassSingleton {
private static ClassSingleton INSTANCE;
private String info = "Initial info class";
private ClassSingleton() {
}
public static ClassSingleton getInstance() {
if(INSTANCE == null) {
INSTANCE = new ClassSingleton();
}
return INSTANCE;
}
// getters and setters
}
Несмотря на то, что это распространённый подход, важно отметить, что он может быть проблематичным в многопоточных сценариях, что и является основной причиной использования синглтонов.
Проще говоря, это может привести к появлению более чем одного экземпляра, что нарушает основной принцип паттерна. Наш следующий подход решает эти проблемы на корневом уровне.
3. Enum Singleton
Двигаясь дальше, давайте обсудим ещё один интересный подход, который заключается в использовании перечислений:
public enum EnumSingleton {
INSTANCE("Initial class info");
private String info;
private EnumSingleton(String info) {
this.info = info;
}
public EnumSingleton getInstance() {
return INSTANCE;
}
// getters and setters
}
При таком подходе сериализация и потокобезопасность гарантируются самой реализацией перечисления (enum), которая внутри обеспечивает доступность только одного экземпляра, исправляя проблемы, указанные в реализации на основе классов.
4. Использование
Чтобы использовать ClassSingleton
, нам просто нужно получить экземпляр статически:
ClassSingleton classSingleton1 = ClassSingleton.getInstance();
System.out.println(classSingleton1.getInfo()); //Initial class info
ClassSingleton classSingleton2 = ClassSingleton.getInstance();
classSingleton2.setInfo("New class info");
System.out.println(classSingleton1.getInfo()); //New class info
System.out.println(classSingleton2.getInfo()); //New class info
Что касается EnumSingleton
, то его можно использовать как любой другой Enum:
EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE.getInstance();
System.out.println(enumSingleton1.getInfo()); //Initial enum info
EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE.getInstance();
enumSingleton2.setInfo("New enum info");
System.out.println(enumSingleton1.getInfo()); // New enum info
System.out.println(enumSingleton2.getInfo()); // New enum info
5. Подводные камни
Синглтон — это обманчиво простой шаблон проектирования, и существует несколько распространённых ошибок, которые программист может совершить при создании синглтона.
Можно выделить два типа проблем с синглтонами:
5.1. Экзистенциальные проблемы
Концептуально синглтон — это разновидность глобальной переменной. В целом, мы знаем, что глобальных переменных следует избегать, особенно если их состояния являются изменяемыми.
Мы не говорим, что не стоит использовать синглтоны; мы полагаем, что могут быть более эффективные способы организации кода.
Если реализация метода зависит от объекта-синглтона, почему бы не передать его в качестве параметра? В этом случае мы явно показываем, от чего зависит метод. В результате мы можем легко имитировать эти зависимости (если необходимо) при тестировании.
Например, синглтоны часто используются для передачи конфигурационных данных приложения (например, подключение к хранилищу). Если они используются как глобальные объекты, становится сложно выбрать конфигурацию для тестовой среды.
Поэтому при запуске тестов в продакшен базу данных попадают тестовые данные, что недопустимо.
Если нам нужен синглтон, мы можем рассмотреть возможность делегирования его инстанцирования другому классу, своего рода фабрике, которая должна позаботиться о том, чтобы в игре был только один экземпляр синглтона.
5.2. Проблемы реализации
Несмотря на то что синглтоны кажутся довольно простыми, при их реализации могут возникать различные проблемы. Все они приводят к тому, что в итоге у нас может оказаться более одного экземпляра класса.
Синхронизация
Реализация с приватным конструктором, которую мы представили выше, не является потокобезопасной. Она хорошо работает в однопоточной среде, но в многопоточной следует использовать технику синхронизации, чтобы гарантировать атомарность операций:
public synchronized static ClassSingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new ClassSingleton();
}
return INSTANCE;
}
Обратите внимание на ключевое слово synchronized
в объявлении метода. Тело метода содержит несколько операций (сравнение, инстанцирование и возврат).
При отсутствии синхронизации есть вероятность того, что два потока чередуют выполнения таким образом, что выражение INSTANCE == null
оценивается как true
для обоих потоков, и в результате создаётся два экземпляра ClassSingleton
.
Синхронизация может существенно повлиять на производительность. Если этот код вызывается часто, следует ускорить его, используя различные техники, такие как ленивая инициализация или блокировка с двойной проверкой (имейте в виду, что это может работать не так, как ожидается, из-за оптимизаций компилятора). Подробнее об этом можно прочитать в статье «Double-Checked Locking with Singleton».
Множественные экземпляры
Есть ещё несколько проблем с синглтонами, связанных с самой JVM — они могут привести к тому, что мы получим несколько экземпляров синглтона. Это довольно тонкие проблемы, и мы дадим краткое описание каждой из них:
Предполагается, что синглтон должен быть уникальным для каждой JVM. Это может быть проблемой для распределённых систем или систем, внутреннее устройство которых основано на распределённых технологиях.
Каждый загрузчик классов может загружать свою версию синглтона.
Синглтон может быть удалён сборщиком мусора, если на него больше никто не держит ссылку. Эта проблема не приводит к наличию нескольких экземпляров синглтона одновременно, но при воссоздании экземпляр может отличаться от своей предыдущей версии.
6. Заключение
В этой небольшой статье мы рассмотрели, как реализовать паттерн Singleton с использованием только Java. Мы узнали, как обеспечить его согласованность и как использовать эти реализации.
Полную реализацию этих примеров можно найти на GitHub.
Материал переведён в преддверии старта онлайн-курса «Java QA Engineer. Basic».