Правильный Singleton в Java
Уверен, каждый из читателей, знает что такое шаблон проектирования “Singleton”, но не каждый знает как его программировать эффективно и правильно. Данная статья является попыткой агрегирования существующих знаний по этому вопросу.
Кроме того, можно рассматривать статью как продолжение замечательного исследования, публиковавшегося на Хабрахабре ранее.
Неленивый Singleton в Java
Автору известно два способа реализации шаблона с нормальной инициализацией.
1 Static field
+ Простая и прозрачная реализация
+ Потокобезопасность
— Не ленивая инициализация
2 Enum Singleton
По мнению Joshua Bloch’а это лучший способ реализации шаблона [1].
+ Остроумно
+ Сериализация из коробки
+ Потокобезопасность из коробки
+ Возможность использования EnumSet, EnumMap и т.д.
+ Поддержка switch
— Не ленивая инициализация
Ленивый Singleton в Java
На момент написания статьи существует как минимум три корректных реализации шаблона Singleton с ленивой инициализацией на Java.
1 Synchronized Accessor
public class Singleton < private static Singleton instance; public static synchronized Singleton getInstance() < if (instance == null) < instance = new Singleton(); >return instance; > >
+ Ленивая инициализация
— Низкая производительность (критическая секция) в наиболее типичном доступе
2 Double Checked Locking & volatile
public class Singleton < private static volatile Singleton instance; public static Singleton getInstance() < Singleton localInstance = instance; if (localInstance == null) < synchronized (Singleton.class) < localInstance = instance; if (localInstance == null) < instance = localInstance = new Singleton(); >> > return localInstance; > >
+ Ленивая инициализация
+ Высокая производительность
— Поддерживается только с JDK 1.5 [5]
2.1 Почему не работает без volatile?
Проблема идиомы Double Checked Lock заключается в модели памяти Java, точнее в порядке создания объектов. Можно условно представить этот порядок следующими этапами [2, 3]:
Пусть мы создаем нового студента: Student s = new Student(), тогда
1) local_ptr = malloc(sizeof(Student)) // выделение памяти под сам объект;
2) s = local_ptr // инициализация указателя;
3) Student::ctor(s); // конструирование объекта (инициализация полей);
Таким образом, между вторым и третьим этапом возможна ситуация, при которой другой поток может получить и начать использовать (на основании условия, что указатель не нулевой) не полностью сконструированный объект. На самом деле, эта проблема была частично решена в JDK 1.5 [5], однако авторы JSR-133 [5] рекомендуют использовать voloatile для Double Cheсked Lock. Более того, их отношение к подобным вещам легко прослеживается из коментария к спецификации:
There exist a number of common but dubious coding idioms, such as the double-checked locking idiom, that are proposed to allow threads to communicate without synchronization. Almost all such idioms are invalid under the existing semantics, and are expected to remain invalid under the proposed semantics.
Таким образом, хотя проблема и решена, использовать Double Checked Lock без volatile крайне опасно. В некоторых случаях, зависящих от реализации JVM, операционной среды, планировщика и т.д., такой подход может не работать. Однако, серией опытов сопровождаемых просмотром ассемблерного кода, генерированного JIT’ом автору, такой случай вопросизвести не удалось.
Наконец, Double Checked Lock можно использовать без исключений с immutable объектами (String, Integer, Float, и т.д.).
3 On Demand Holder idiom
public class Singleton < public static class SingletonHolder < public static final Singleton HOLDER_INSTANCE = new Singleton(); >public static Singleton getInstance() < return SingletonHolder.HOLDER_INSTANCE; >>
+ Ленивая инициализация
+ Высокая производительность
— Невозможно использовать для не статических полей класса
Performance
Для сравнения производительности выше рассмотренных методов, была использована микро-бенчмарка [6], определяющая количество элементарных операций (инкремент поля) в секунду над Singleton объектом, из двух параллельных потоков.
Измерения производились на двухядерной машине Intel Core 2 Duo T7300 2GHz, 2Gb ram и Java HotSpot(TM) Client VM (build 17.0-b17). За единицу скора считается количество инкрементов (а следовательно и захватов объекта) в секунду * 100 000.
(больше — лучше)
Client | Server | |
---|---|---|
Synchronized accessor | 42,6 | 86,3 |
Double Checked Lock & volatile | 179,8 | 202,4 |
On Demand Holder | 181,6 | 202,7 |
Вывод: если правильно подобрать реализацию шаблона можно получить ускорение (speed up) от 2х до 4х.
Summary
Можно выделить следующие короткие советы по использованию того или иного подхода для реализации шаблона “Одиночка” [1].
1) Использовать нормальную (не ленивую) инициализацию везде где это возможно;
2) Для статических полей использовать On Demand Holder idom;
3) Для простых полей использовать Double Chedked Lock & volatile idom;
4) Во всех остальных случаях использовать Syncronized accessor;
Java Class Library & Singleton
Примечательно, что разработчики Java Class Library выбрали наиболее простой способ реализации шаблона — Syncronized Accessor. C одной стороны — это гарантия совместимости и правильной работы. С другой — это потеря процессорного времени на вход и выход из критической секции при каждом обращении.
Быстрый поиск grep’ом по исходникам дал понять, что таких мест в JCL очень много.
Возможно следующая статья будет “Что будет если в Java Class Library правильно написать все Singleton классы?” 🙂
Паттерны проектирования: Singleton
Привет! Сегодня будем подробно разбираться в разных паттернах проектирования, и начнем с шаблона Singleton, который еще называют “одиночка”. Давай вспомним: что мы знаем о шаблонах проектирования в целом? Шаблоны проектирования — это лучшие практики, следуя которым можно решить ряд известных проблем. Шаблоны проектирования как правило не привязаны к какому-либо языку программирования. Воспринимай их как свод рекомендаций, следуя которым можно избежать ошибок и не изобретать свой велосипед.
Что такое синглтон?
- Дает гарантию, что у класса будет всего один экземпляр класса.
- Предоставляет глобальную точку доступа к экземпляру данного класса.
- Приватный конструктор. Ограничивает возможность создания объектов класса за пределами самого класса.
- Публичный статический метод, который возвращает экземпляр класса. Данный метод называют getInstance . Это глобальная точка доступа к экземпляру класса.
Варианты реализации
- Ленивая инициализация: когда класс загружается во время работы приложения именно тогда, когда он нужен.
- Простота и прозрачность кода: метрика, конечно, субъективная, но важная.
- Потокобезопасность: корректная работа в многопоточной среде.
- Высокая производительность в многопоточной среде: потоки блокируют друг друга минимально, либо вообще не блокируют при совместном доступе к ресурсу.
- Не ленивая инициализация: когда класс загружается при старте приложения, независимо от того, нужен он или нет (парадокс, в мире IT лучше быть лентяем)
- Сложность и плохая читаемость кода. Метрика также субъективная. Будем считать, что если кровь пошла из глаз, реализация так себе.
- Отсутствие потокобезопасности. Иными словами, “потокоопасность”. Некорректная работа в многопоточной среде.
- Низкая производительность в многопоточной среде: потоки блокируют друг друга все время либо часто, при совместном доступе к ресурсу.
Код
public class Singleton < private static final Singleton INSTANCE = new Singleton(); private Singleton() < >public static Singleton getInstance() < return INSTANCE; >>
- Простота и прозрачность кода
- Потокобезопасность
- Высокая производительность в многопоточной среде
- Не ленивая инициализация.
public class Singleton < private static Singleton INSTANCE; private Singleton() <>public static Singleton getInstance() < if (INSTANCE == null) < INSTANCE = new Singleton(); >return INSTANCE; > >
public class Singleton < private static Singleton INSTANCE; private Singleton() < >public static synchronized Singleton getInstance() < if (INSTANCE == null) < INSTANCE = new Singleton(); >return INSTANCE; > >
- Ленивая инициализация.
- Потокобезопасность
- Низкая производительность в многопоточной среде
public class Singleton < private static Singleton INSTANCE; private Singleton() < >public static Singleton getInstance() < if (INSTANCE == null) < synchronized (Singleton.class) < if (INSTANCE == null) < INSTANCE = new Singleton(); >> > return INSTANCE; > >
- Ленивая инициализация.
- Потокобезопасность
- Высокая производительность в многопоточной среде
- Не поддерживается на версиях Java ниже 1.5 (в версии 1.5 исправили работу ключевого слова volatile)
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 .
Реализация | Ленивая инициализация | Потокобезопасность | Скорость работы при многопоточности | Когда использовать? |
---|---|---|---|---|
Simple Solution | — | + | Быстро | Никогда. Либо когда не важна ленивая инициализация. Но лучше никогда. |
Lazy Initialization | + | — | Неприменимо | Всегда, когда не нужна многопоточность |
Synchronized Accessor | + | + | Медленно | Никогда. Либо когда скорость работы при многопоточности не имеет значения. Но лучше никогда |
Double Checked Locking | + | + | Быстро | В редких случаях, когда нужно обрабатывать исключения при создании синглтона. (когда неприменим Class Holder Singleton) |
Class Holder Singleton | + | + | Быстро | Всегда, когда нужна многопоточность и есть гарантия, что объект синглтон класса будет создан без проблем. |
Плюсы и минусы паттерна Singleton
- Дает гарантию, что у класса будет всего один экземпляр класса.
- Предоставляет глобальную точку доступа к экземпляру данного класса.
- Синглтон нарушает SRP (Single Responsibility Principle) — класс синглтона, помимо непосредственных обязанностей, занимается еще и контролированием количества своих экземпляров.
- Зависимость обычного класса или метода от синглтона не видна в публичном контракте класса.
- Глобальные переменные это плохо. Синглтон превращается в итоге в одну здоровенную глобальную переменную.
- Наличие синглтона снижает тестируемость приложения в целом и классов, которые используют синглтон, в частности.