Java. Шаблон Singleton
Шаблон не является частью языка Java, это рекомендация умных программистов для создания эффективных решений во время разработки сложных проектов.
Синглтон (singleton) это класс, у которого экземпляр создаётся только один раз. Хорошим примером такого поведения служит файловая система, система работы с видео и т.д. В Android можно привести примеры с классами OkHttpClient, Retrofit, Gson, SharedPreferences. Для реализации синглтона нужно создать закрытый конструктор и открытый статический член, который и позволяет получить доступ к единственному экземпляру класса. Например, так.
public class Single < public static final Single INSTANCE = new Single(); private Single()<>public void someMethod() < Log.i("Log", "I am someMethod"); >>
Закрытый конструктор вызывается один раз для инициализации поля INSTANCE. Открытых конструктор у класса нет, поэтому после инициализации класса будет существовать только один экземпляр Single.
Существует другой вариант, когда вместо открытого статического поля создаётся открытый статический метод, а поле становится закрытым.
public class Single < private static final Single INSTANCE = new Single(); private Single()<>public void someMethod() < Log.i("Log", "I am a someMethod"); >public static Single getInstance() < return INSTANCE; >>
Второй способ удобен тем, что вы можете позже отказаться от синглтона, но вам не придётся сильно переписывать код.
Позже был придуман третий вариант с использованием enum.
Правильный Singleton в Java
Уверен, каждый из читателей, знает что такое шаблон проектирования “Singleton”, но не каждый знает как его программировать эффективно и правильно. Данная статья является попыткой агрегирования существующих знаний по этому вопросу.
Кроме того, можно рассматривать статью как продолжение замечательного исследования, публиковавшегося на Хабрахабре ранее.
Неленивый Singleton в Java
Автору известно два способа реализации шаблона с нормальной инициализацией.
1 Static field
public class Singleton
+ Простая и прозрачная реализация
+ Потокобезопасность
— Не ленивая инициализация
2 Enum Singleton
По мнению Joshua Bloch’а это лучший способ реализации шаблона [1].
public enum Singleton
+ Остроумно
+ Сериализация из коробки
+ Потокобезопасность из коробки
+ Возможность использования 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 классы?” 🙂
- java
- design patterns
- singleton
- double checked lock
Паттерны проектирования: Singleton — Часть1
В предыдущей статье «Что такое паттерны проектирования в Java» мы рассмотрели что такое паттерн и какие есть паттерны проектирования. В этой статье мы расскажем об одном из самых распространенных паттернов проектирования — Singleton.
Данная статья рассчитана на тех, кто уже хорошо знает ООП.
Что такое Singleton?
Что такое Singleton можно объяснить одной фразой:

Звучит логично, правда? Мы привыкли, что обычно создаем несколько объектов одного класса — вспомним хотя бы String. Но иногда наоборот — что-то необходимо использовать только ОДИН раз. Можно найти примеры этого и в реальной жизни:
- Вы не берете новую ручку каждый раз, когда пишете новое слово?
- Вы не покупаете новый автомобиль каждый раз, когда нужно куда-то ехать?
Вот так и в программировании — иногда нам хватает только одного экземпляра класса для того, чтобы выполнить все поставленные задачи.
Итак, представьте, что в программе Вам нужно:
- получить класс, который имеет всего одинэкземпляр класса
- в таком случае все, кто обращается к этому классу, будут получать один и тот же экземпляр.
- желательно — для потокобезопасности — должен быть stateless (информацию про stateless можно прочитать тут).
Именно такой класс называется Singleton-ом.
Чем Singleton НЕ ЯВЛЯЕТСЯ?
- Singleton — это не метод, а класс.
- Singleton — это не готовый рецепт создания класса. Есть несколько способов сделать класс синглтоном — мы их рассмотрим далее. Но «начинка» этих классов остается за Вами.
Может ли меняться Singleton?
Да, состояние Singleton-а может быть как изменяемым, так и не изменяемым.
Представьте, что у Вас есть сосед Вася. Вася хакер и хочет взломать Вашу почту. Он подбирает пароли — один за другим — и вводит их по очереди.

Именно для защиты от таких Вась и была придумана блокировка. То есть, например, ввел 5 раз неправильно пароль, все — ты заблокирован — и 30 минут не можешь вводить никакие пароли.
А теперь представим, что именно Вам нужно реализовать систему защиты от таких вот Вась на своем сервере.
Обратите внимание, что множественный ввод не так легко отследить как кажется. На Ваш сервер одновременно поступает огромное количество информации.

Кроме того, Вася может пытаться вводить пароль с разных IP-адресов, что еще больше усложняет задачу. А нам ведь нужно как-то контролировать количество попыток входа в конкретную учетную запись.
В такой ситуации нам как раз и поможет Singleton!

Каждый раз, когда будет запрашиваться доступ к определенной учетной записи, мы будем перенаправлять этот запрос на некий класс, реализующий паттерн Singleton (ведь нам же надо хранить эту информацию только в единственной «точке»). Данный класс будет считать количество неудачных попыток зайти на сайт.
В данном случае наш Singleton будет изменяемым — каждый раз, когда будет осуществляться попытка доступа к нашей учетной записи, переменная, которая считает количество попыток, будет меняться.
Так, чтобы решить эту проблему, нам понадобился изменяемый Singleton.
Пример 2 — неизменяемый Singleton

Одним из примеров неизменяемого Singleton-а может быть класс, который ставит водяной знак (watermark) на изображения, чтобы предотвратить их кражу.
В принципе, поставить вотермарку на изображение — задача достаточно простая. Классу нужно только получить изображение и отдать уже отредактированную картинку. Если водяной знак будет один и тот же на всех изображениях, тогда нет необходимости менять что-либо и в состоянии класса, поэтому он будет неизменяемым. С этой задачей сможет справиться один экземпляр этого класса, а значит можем создать класс на основе паттерна Singleton, который будет выполнять одну и ту же задачу — ставить водяные знаки на изображения.

Несмотря на то, что паттерн Singleton может показаться легким в теории, на практике можно наткнуться на «подводные камни». Давайте посмотрим на примерах.
Как и когда создавать Singleton?
- «Я создаю класс. Мне нужно будет несколько инстансов в будущем или нет?»
Любой класс, которого Вам понадобится только один экземпляр можно сделать Singleton -ом. Вот подумайте — сколько памяти Вы сэкономите. Сколько времени. Ммм. Красота.
- Должен ли Singleton быть доступен из любой точки программы?
В Интернете часто можно увидеть, что Singleton якобы должен быть доступен «из любой точки программы». Это не совсем так. Singleton, как и любые другие данные, должны быть доступны только там, где они нужны. Если они нужны в части программы А — нет необходимости чтобы ее видели части В, C и D.
Тем не менее, обратите внимание на то, какая видимость должна быть у Вашего Singleton-а.
- Обратите внимание — нужно ли Вам работать с потоками.
В зависимости от того, работаете Вы с потоками или нет, можно будет выбрать оптимальную реализацию Singleton-а.
Пример — Самый простой вариант реализации
Реализация Singleton в JAVA
В этой статье я хочу затронуть тему одного из наиболее распространенных паттернов объектно-ориентированного программирования – Singleton. Но в данном случае я не буду описывать преимущества/недостатки и области применения этого паттерна, а попытаюсь изложить свой взгляд на его имплементацию в JAVA.
Общие сведения
Паттерн Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.
Область применения
1.) В системе должно существовать не более одного экземпляра заданного класса.
2.) Экземпляр должен быть легко доступен для всех клиентов данного класса.
3.) Создание объекта on demand, то есть, когда он понадобится первый раз, а не во время инициализации системы.
Реализация (JAVA):
На данный момент существуют несколько вариантов реализации со своими недостатками и преимуществами. В них мы и попробуем сейчас разобраться.

Вариант первый – самый простой, который приходит в голову сразу после понимания проблемы.
У этого решения есть единственный недостаток – оно не работает в многопоточной среде и поэтому не подходит в большинстве случаев. Решение подходит исключительно для однопоточных приложений.
Не беда, скажите вы, и предложите следующее решение.

Вариант второй:
И вы будете правы, так как проблему многопоточности мы решили, но потеряли две важные вещи:
1. Ленивую инициализацию (Объект instance будет создан classloader-ом во время инициализации класса)
2. Отсутствует возможность обработки исключительных ситуаций(exceptions) во время вызова конструктора.
Решение подходит для многопоточных приложений, при условии отсутствия опасности возникновения исключительных ситуаций в конструкторе и отсутствии необходимости ленивой инициализации.
Далее возникают 2 варианта решения.
1.) Использование внутреннего класса(решение Била Пью(Bill Pugh) “Initialization on Demand Holder”).
2.) Использование синхронизации.
Начнем с Била Пью.

Вариант третий:
“Initialization on Demand Holder”
В данном случае мы полностью решили проблему ленивой инициализации – объект инициализируется при первом вызове метода getInstance(). Но у нас осталась проблема с обработкой исключительных ситуаций в конструкторе. Так что, если конструктор класса не вызывает опасений создания исключительных ситуаций, то смело можно использовать этот метод.
Синхронизация
Этой части я хотел бы уделить особое внимание. Можно было бы подойти к данному вопросу с заголовком «synchronized – мифы и реальность».
И так, самый прямолинейный метод.

Вариант четвертый:
У этого варианта есть только один недостаток. Синхронизация полезна только один раз, при первом обращении к getInstance(), после этого каждый раз, при обращении этому методу, синхронизация просто забирает время. Что можно сказать по этому поводу? Ну, во-первых, если вызов getInstance() не происходит достаточно часто (что значит «достаточно часто» решать вам), то этот метод имеет преимущество перед остальными – прост, понятен, лениво инициализируется, дает возможность обрабатывать исключительные ситуации в конструкторе. А во-вторых, синхронизация в Java перестала быть обременительно медленной настолько, насколько ее боятся. Ну что еще для счастья надо?
Теперь рассмотрим вариант синхронизированного решения, в котором попытаемся решить проблему, возникшую в предыдущем варианте.

Наиболее распространенный способ — «Double-Checked Locking». В своем оригинальном варианте:
Не работает! Почему? Это отдельная тема, но для интересующихся могу посоветовать прочитать эту статью http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.
Но не надо совсем отчаиваться, в JAVA 5 проблему решили, используя модификатор volatile. На данный момент решение выглядит так:

Вариант пятый:
Не смотря на то, что этот вариант выглядит как идеальное решение, использовать его не рекомендуется т.к. товарищ Allen Holub заметил, что использование volatile модификатора может привести к проблемам производительности на мультипроцессорных системах. Но решать все же вам.
Вот, в общем-то, и все распространенные варианты имплементаций данного паттерна. Но, на этом не заканчиваются подводные камни Singleton-а. Существуют еще несколько моментов, которые нужно учитывать во время проектирования того или иного приложения, использующего Singleton.
Подводные камни
1. Наследование
В подавляющем большинстве случаев в Singleton классах наследование не нужно и, более того, излишне и является следствием over-design. Да и реализация наследования имеет определенные сложности, учитывая, что и сам instance и метод getInstance() статические.
Поэтому, я рекомендую использовать модификатор final и запретить наследование данного класса, если нет особой необходимости в обратном.
2. Две и более виртуальных машины
Каждая виртуальная машина создает свою копию Singleton объекта. И хотя на первый взгляд это выглядит очевидным, во многих распределенных системах, таких как EJB, JINI и RMI все не так просто. Когда промежуточные уровни скрывают (делают прозрачными) распределенные технологи, бывает трудно сказать, где и когда инициализирован объект.
3.Различные Class Loader-ы
Когда 2 class loader-а загружают класс, каждый из них может создать свою копию Singleton-а(в тех случаях, когда instance инициализируется class loader-ом ). Это особенно актуально в использовании сервлетов(servlet), так как в некоторых имплементациях серверов приложений(application server) каждый сервлет имеет свой class loader.
Существует еще ряд проблем, которые менее актуальны (такие как технология reflection и имплементация интерфейсов Cloneable и Serializable), и не будут мною рассмотрены в силу своей экзотичности в сфере применения Singleton классов. Но, в любом случае, с радостью отвечу на любые вопросы к этому материалу.
Вот, в общем-то, и все ключевые моменты, которые я хотел осветить в данной статье. Осталось лишь заметить, что данный материал не претендует быть истиной в последней инстанции и допускает существование точек зрения, отличающихся от точки зрения автора. И даже более того, любые замечания приветствуются и будут прияты к сведению.
Спасибо за внимание.