getInstance() отдает всем один и тот же экземпляр. Реализаций у синглтона несколько, и различаются они только тем, в какой момент создается объект и что происходит, когда за ним приходят сразу несколько потоков.
Давай вспомним: что мы знаем о шаблонах проектирования в целом?
Шаблоны проектирования это лучшие практики, следуя которым можно решить ряд известных проблем.
Шаблоны проектирования как правило не привязаны к какому-либо языку программирования. Воспринимай их как свод рекомендаций, следуя которым можно избежать ошибок и не изобретать свой велосипед.Кратко
- Синглтон гарантирует, что у класса будет ровно один объект, и дает к нему глобальную точку доступа.
- Механика везде одна: приватный конструктор плюс публичный статический метод
getInstance(). - Реализации отличаются двумя вещами: когда создается объект, сразу или при первом обращении, и что происходит при доступе из нескольких потоков.
- В статье разобраны шесть вариантов: Simple Solution, Lazy Initialization, Synchronized Accessor, Double Checked Locking, Class Holder Singleton и Enum Singleton.
- Рабочий выбор по умолчанию это Class Holder Singleton: ленивый, потокобезопасный и быстрый, причем без единой строчки синхронизации в коде. Если ленивость не нужна, надежнее синглтон на
enum: только он не ломается рефлексией и сериализацией. - У паттерна есть цена: нарушение принципа единственной ответственности, скрытая зависимость, глобальное состояние и просевшая тестируемость.
Что такое синглтон?
Синглтон это один из самых простых шаблонов (паттернов) проектирования, который применяется к классу. Иногда говорят: “этот класс синглтон”, подразумевая, что этот класс реализует паттерн проектирования синглтон. Иногда необходимо написать класс, у которого можно будет создать только один объект. Например, класс, отвечающий за логирование или подключение к базе данных. Шаблон проектирования синглтон описывает, как мы можем выполнить такую задачу. Синглтон это шаблон (паттерн) проектирования, который делает две вещи:Дает гарантию, что у класса будет всего один экземпляр класса.
Предоставляет глобальную точку доступа к экземпляру данного класса.
Приватный конструктор. Ограничивает возможность создания объектов класса за пределами самого класса.
Публичный статический метод, который возвращает экземпляр класса. Данный метод называют
getInstance. Это глобальная точка доступа к экземпляру класса.
Варианты реализации
Шаблон проектирования синглтон применяют по-разному. Каждый вариант по-своему хорош и плох. Тут как всегда: идеала нет, но нужно к нему стремиться. Но прежде всего давай определимся, что такое хорошо и что такое плохо, и какие метрики влияют на оценку реализации шаблона проектирования. Начнем с положительного. Вот критерии, которые придают реализации сочности и привлекательности:Ленивая инициализация: когда класс загружается во время работы приложения именно тогда, когда он нужен.
Простота и прозрачность кода: метрика, конечно, субъективная, но важная.
Потокобезопасность: корректная работа в многопоточной среде.
Высокая производительность в многопоточной среде: потоки блокируют друг друга минимально, либо вообще не блокируют при совместном доступе к ресурсу.
Не ленивая инициализация: когда класс загружается при старте приложения, независимо от того, нужен он или нет (парадокс, в мире IT лучше быть лентяем)
Сложность и плохая читаемость кода. Метрика также субъективная. Будем считать, что если кровь пошла из глаз, реализация так себе.
Отсутствие потокобезопасности. Иными словами, “потокоопасность”. Некорректная работа в многопоточной среде.
Низкая производительность в многопоточной среде: потоки блокируют друг друга все время либо часто, при совместном доступе к ресурсу.
Код
Теперь мы готовы рассмотреть различные варианты реализации с перечислением плюсов и минусов:Simple Solution
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
Самая простая реализация.
Плюсы:
Простота и прозрачность кода
Потокобезопасность
Высокая производительность в многопоточной среде
- Не ленивая инициализация.
Singleton, пусть даже это будет обращение к какой-нибудь его константе. Разница с ленивыми вариантами в том, что моментом создания ты не управляешь.
В попытке исправить последний недостаток, мы получаем реализацию номер два:
Lazy Initialization
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
Плюсы:
Ленивая инициализация.
Не потокобезопасно
Synchronized Accessor
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
Плюсы:
Ленивая инициализация.
Потокобезопасность
Низкая производительность в многопоточной среде
getInstance синхронизирован, и входить в него можно только по одному.
На самом деле нам нужно синхронизировать не весь метод, а лишь ту его часть, в которой мы инициализируем новый объект класса. Но мы не можем просто обернуть в synchronized блок часть, отвечающую за создание нового объекта: это не обеспечит потокобезопасность. Все немного сложнее.
Правильный способ синхронизации представлен ниже:
Double Checked Locking
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
Плюсы:
Ленивая инициализация.
Потокобезопасность
Высокая производительность в многопоточной среде
Многословно и легко ошибиться: достаточно забыть
volatile, и реализация тихо перестает быть потокобезопасной, а поймать такую ошибку тестами почти невозможно.
volatile в объявлении поля: без него эта реализация не работает. Строка INSTANCE = new Singleton() это не одно действие, а три: выделить память, отработать конструктор, записать ссылку в поле. Два последних шага компилятор или процессор вправе поменять местами, и тогда соседний поток увидит в INSTANCE ссылку на еще недостроенный объект, причем даже не заходя в блок synchronized, ведь первая проверка на null его туда не пустит. Ключевое слово volatile такую перестановку запрещает, и начиная с Java 5 этого достаточно, о чем прямо говорит декларация «Double-Checked Locking is Broken». Вариант с final здесь не подойдет: статическое финальное поле обязано получить значение при инициализации класса, а вся идея ленивой реализации в том, чтобы этого не делать.
Следующая реализация, которую мы обсудим, это Class Holder Singleton. Построена она на статическом вложенном классе.
Class Holder Singleton
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
public static final Singleton HOLDER_INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.HOLDER_INSTANCE;
}
}
Плюсы:
Ленивая инициализация.
Потокобезопасность.
Высокая производительность в многопоточной среде.
Для корректной работы необходима гарантия, что объект класса
Singletonинициализируется без ошибок. Иначе первый вызов методаgetInstanceзакончится ошибкойExceptionInInitializerError, а все последующиеNoClassDefFoundError.
Enum Singleton
public enum Singleton {
INSTANCE;
public void doWork() {
// полезная работа синглтона
}
}
Точка доступа тут не getInstance(), а сама константа: Singleton.INSTANCE.doWork().
Плюсы:
Простота и прозрачность кода: конструктор прятать не нужно, компилятор делает это сам.
Потокобезопасность: константа создается при инициализации класса, а за нее отвечает сама виртуальная машина.
Высокая производительность в многопоточной среде: синхронизации в коде нет вовсе.
Единственный из шести вариантов, который не ломается ни рефлексией, ни сериализацией.
Не ленивая инициализация, ровно в том же смысле, что и у Simple Solution.
Такой синглтон нельзя унаследовать от другого класса: перечисление уже наследуется от
java.lang.Enum. Интерфейсы реализовывать при этом можно.
new. Рефлексия обходит приватный конструктор через setAccessible(true), а десериализация делает новый объект, если не написать метод readResolve(). С перечислением не работает ни то, ни другое: Constructor.newInstance() бросает IllegalArgumentException, когда конструктор принадлежит enum-классу, а ObjectInputStream не создает константу заново, а находит существующую по имени через Enum.valueOf().
Сравнительная таблица различных реализаций паттерна Singleton:
| Реализация | Ленивая инициализация | Потокобезопасность | Скорость работы при многопоточности | Когда использовать? |
|---|---|---|---|---|
| Simple Solution | - | + | Быстро | Никогда. Либо когда не важна ленивая инициализация. Но лучше никогда. |
| Lazy Initialization | + | - | Неприменимо | Всегда, когда не нужна многопоточность |
| Synchronized Accessor | + | + | Медленно | Никогда. Либо когда скорость работы при многопоточности не имеет значения. Но лучше никогда |
| Double Checked Locking | + | + | Быстро | В редких случаях, когда нужно обрабатывать исключения при создании синглтона. (когда неприменим Class Holder Singleton) |
| Class Holder Singleton | + | + | Быстро | Всегда, когда нужна многопоточность и есть гарантия, что объект синглтон класса будет создан без проблем. |
| Enum Singleton | - | + | Быстро | Когда надежность важнее всего: единственный вариант, устойчивый к рефлексии и сериализации |
Плюсы и минусы паттерна Singleton
В целом синглтон делает именно то, что от него ждут:Дает гарантию, что у класса будет всего один экземпляр класса.
Предоставляет глобальную точку доступа к экземпляру данного класса.
Синглтон нарушает SRP (Single Responsibility Principle): класс синглтона, помимо непосредственных обязанностей, занимается еще и контролированием количества своих экземпляров.
Зависимость обычного класса или метода от синглтона не видна в публичном контракте класса.
Глобальные переменные это плохо. Синглтон превращается в итоге в одну здоровенную глобальную переменную.
Наличие синглтона снижает тестируемость приложения в целом и классов, которые используют синглтон, в частности.
Вопросы и ответы
Что такое синглтон простыми словами?
Это класс, у которого во всей программе существует ровно один объект. Обычный класс можно создавать сколько угодно раз, а синглтон прячет конструктор и выдает единственный готовый экземпляр через статический методgetInstance(). Так поступают, например, с логгером или с подключением к базе данных: второй такой объект не нужен, а иногда и вреден.
Как сделать класс синглтоном в Java?
Нужны три вещи. Приватный конструктор, чтобы снаружи класс нельзя было создать черезnew. Приватное статическое поле, в котором лежит единственный экземпляр. И публичный статический метод getInstance(), который этот экземпляр отдает. Дальше начинаются варианты: создавать объект при инициализации класса или при первом вызове getInstance(), и как защитить создание от нескольких потоков. Есть и путь короче: объявить enum с единственной константой, тогда всю работу берет на себя компилятор.
Почему Double Checked Locking не работает без volatile?
Потому что запись ссылки и заполнение полей объекта могут поменяться местами. СтрокаINSTANCE = new Singleton() раскладывается на три шага: выделить память, отработать конструктор, записать ссылку в поле. Компилятор и процессор вправе переставить два последних, и тогда другой поток увидит в INSTANCE ненулевую ссылку на недостроенный объект, даже не заходя в synchronized: внешняя проверка на null его туда не пустит. Слово volatile запрещает такую перестановку, и с Java 5 этого достаточно, чтобы идиома работала.
Какая реализация синглтона лучше?
Если нужна именно ленивая инициализация, то Class Holder Singleton: объект создается при первом обращении к вложенному классу-держателю, потокобезопасность обеспечивает сама виртуальная машина, а синхронизации в коде нет, поэтому нет и потерь скорости. Если ленивость не критична, надежнее синглтон наenum: он короче всех и единственный не ломается рефлексией и сериализацией. Double Checked Locking остается для случая, когда конструктор синглтона может бросить исключение и его надо обработать.
Чем плох синглтон?
Тем, что это глобальная переменная с хорошими манерами. Класс совмещает свою прямую работу с контролем над числом собственных экземпляров, а это нарушение принципа единственной ответственности. Зависимость от синглтона не видна в сигнатурах: класс выглядит самостоятельным, а внутри дергаетgetInstance(). И тестировать такой код тяжело, потому что подменить синглтон заглушкой обычно нечем.
Можно ли сломать синглтон через рефлексию или сериализацию?
Да, и это работает против всех реализаций из статьи, кроме последней. Рефлексия позволяет добраться до приватного конструктора черезsetAccessible(true) и создать второй объект. Десериализация по умолчанию тоже создает новый экземпляр, если не написать метод readResolve(). Устойчив к обоим способам только синглтон на enum: конструктор перечисления рефлексии недоступен, Constructor.newInstance() бросает на нем IllegalArgumentException, а при десериализации константа не создается заново, а находится по имени через Enum.valueOf().
Читайте также
- Паттерн проектирования Factory
- Паттерны проектирования: FactoryMethod
- Паттерн проектирования Proxy
- Паттерны проектирования в Java