Паттерны проектирования: Singleton - Часть 2

Facebooktwittergoogle_plustumblrFacebooktwittergoogle_plustumblr

Данная статья:


Привет!

В предыдущей статье "Паттерны проектирования: Singleton - Часть 1" мы рассмотрели в общих чертах что такое паттерн Singleton. 

В этой статье мы расскажем 5 реализаций Singleton-а:

1. Самый простой вариант реализации

2. Ленивая реализация

3. Ленивая реализация с быстрой инициализацией

4. Хитрая реализация с внутренним классом

5. Самая хитрая реализация

 

Пример 1  - Самый простой вариант 

Итак, что произошло? У нас есть ПРИВАТНЫЙ конструктор, а это значит, что:

1. Мы не сможем создать объект класса как мы это привыкли делать. Например, если мы запустим такой код:

Получим ошибку:

Итак, наш приватный конструктор не позволяет никому создавать новый экземпляр класса. Но как тогда вообще создать хотя бы первый объект? Это происходит внутри самого класса - в этой строке:

Как Вы могли заметить, INSTANCE имеет доступ public. Это значит, что мы можем обращаться к нему из любого другого класса.

2. Обратите внимание, что мы сделали приватным конструктор по умолчанию. Это значит, что мы не сможем наследовать класс Singleton.

Например, создадим какой-нибудь класс, который будет наследовать наш Singleton:

Получим ошибку:

А теперь давайте попробуем реализовать его и запустить.

Реализация

Пишем такой Singleton:

Тут все как и в примере выше. Но чтобы было интереснее, мы добавили метод printName(). Он печатает строку "I am a Singleton!".

Теперь, запустим следующий код:

Получаем:

Комментарии к коду:

  • Так как первым делом при загрузке класса был создан INSTANCE, первым в консоли мы видим сообщение из конструктора - "Singleton created!".
  • В main мы обратились к нашему единственному экземпляру -  INSTANCE, через название класса:

MySingleton.INSTANCE

  • Сначала мы вызвали метод printName():

MySingleton.INSTANCE.printName();

В консоли получаем "I am a Singleton!".

  • Что еще делать с этим Singleton-ом? 🙂 Применяем на нем универсальный метод getClass() и выводим в консоль:

System.out.println(MySingleton.INSTANCE.getClass());

В консоли получаем "class MySingleton".

Поздравляем - теперь мы реализовали наш первый простейший Singleton!

Пример 2  - Ленивая реализация

  • Почему "ленивая"?

Реализация называется ленивой не потому, что Вы можете сэкономить на ней время и пойти попить кофе, а потому, что объект Singleton-а создается не сразу при загрузке программы, а только "по вызову".

  • Зачем?

"Ленивая инициализация" полезна, когда создание объекта класса Singleton очень ресурсоемкое. Оно может тормозить программу, если будет выполняться при запуске. Мы же создаем объект только когда первый раз вызываем специальный метод.

Код:

Давайте попробуем реализовать его и запустить.

Реализация

Напишем такой класс:

Запустим его с помощью следующего кода:

Получаем:

Комментарии к коду:

Теперь наш instance создается не при загрузке программы, а только по вызову.

Что мы имеем в коде?

  • Сначала мы запрашиваем instance с помощью метода getInstance(). В консоли видите сообщение, которое появляется при вызове конструктора - "Singleton created!"
  • Потом используем наш firstInstance - например, применяем на нем метод getClass() и выводим в консоль. Получаем "class MySingleton".
  • Второй раз вызываем метод getInstance(). Как и ожидалось, новый объект не создается. Поэтому, в консоли мы не видим нового сообщения "Singleton created!" (оно появится только при вызове конструктора).
  • Кроме того, мы сравниваем firstInstance и secondInstance. Как видите, они равны - т.е. мы получаем один и тот же объект. Значит, наш Singleton настоящий! 🙂
  • Обратите внимание на модификатор synchronized возле метода getInstance(). Он нужен для корректной работы в многопоточной среде. Но теперь этот метод - "узкое место" в нашей программе. Только один поток в один момент времени может получить к нему доступ - а это замедляет выполнение программы. Если хотите больше узнать про synchronized - прочитайте эту статью.

Пример 3  -  Ленивая реализация с быстрой инициализацией

Давайте попробуем реализовать его и запустить.

Реализация

Представим, что у нас есть такой класс:

Запускаем:

Получаем:

Комментарии к коду:

В принциапе, вывод мало чем отличается от вывода в примере номер два. Давайте лучше разберемся с кодом самого Singleton-а:)

  • Во-первых, у нас есть переменная с модификатором volatile. Этот модификатор делает так, что каждый поток не создает свою копию переменной, а все потоки работают с одной и той же. Действительно, зачем нам копии Singleton-а:) Он же Singleton!
  • Дальше у нас есть метод getInstance(). Давайте еще раз на него посмотрим:

Много условий, да? 🙂 Они нужны для обеспечения работы в многопоточной среде. Давайте рассмотрим два варианта - сначала более легкий на примере одного потока, а потом покажем как будет выполняться код если к методу будут обращаться одновременно несколько потоков.

  1. Сначала рассмотрим легкий вариант - когда у нас есть только один поток. Представим, что объект класса MySingleton еще не создан - то-есть наш instance = NULL. Что будет происходить?

Наш поток зайдет в метод getInstance(). Как мы уже говорили, наш instance равен NULL - поэтому мы заходим в первое условие.

Потом мы видим что? Синхронизированный блок с ключом MySingleton.class. Если мы синхронизируем блок по ключу, это значит что в данный момент времени только один поток может иметь доступ к синхронизированным блкам с таким ключом и начинает "владеть" указанным ключом (в нашем случае - MySingleton.class). Что такое объект типа класс мы рассмотрим в отдельной статье - а сейчас давайте воспринимать MySingleton.class просто как "чертеж", по которому создается конкретный класс.  Без такого чертежа мы не можем создавать новый класс. И что это значит? Это значит что как только наш поток зашел в synchronized блок, новый объект данного класса не может быть создан никем, кроме нашего потока.

Итак, наш поток зашел в синхронизированнй блок. Теперь у него есть "чертеж" класса,  и никто не может создать новый объект класса MySingleton кроме него.

Теперь мы заходим во второй if:

Как мы уже говорили, наш instance пока равен NULL. Поэтому происходит что? Мы создаем новый объект класса  MySingleton() - и наш instance уже не равен null. Отлично!

Что теперь произойдет при повторном вызове метода getInstance()? Мы даже не зайдем в первый if потому что наш instance уже не пустой. Мы сразу вернем его значение.

2. А теперь давайте рассмотрим вариант если у нас несколько потоков. Опять же, начнем сначала - представим, что наш  instance пока равен NULL.

Представим, что несколько потоков заходят в первый if:

Потом у нас что? Синхронизированный блок! В нем может находится только один поток в один момент времени. Это значит, что один поток который успел первым, зайдет в блок, а остальные будут ждать "снаружи" 🙂

Что происходит внутри синхронизированного метода? В принципе, тут все как и выше - поток заходит во второе условие и создает новый объект класса MySingleton().

Отлично! Теперь наш первый поток вышел из синхронизированного блока и получает созданный instance.

Что происходит дальше? Как только первый поток вышел из синхронизированного блока, в него заходит следующий. Остальные потоки все еще ждут "снаружи".

Как Вы помните, мы уже создали объект класса MySingleton() и теперь instance не равен NULL. Поэтому этот поток не заходит во второй if, а просто возвращает instance. Так же и другие потоки - поскольку наш instance не равен NULL, а уже был инициализирован самым первым потоком, они вернут готовый instance.

Для этого нужна вторая проверка, что бы наш синглтон создавался только один раз даже в многопоточной среде.

Пример 4  - Хитрая реализация с внутренним классом

Давайте попробуем реализовать его и запустить.

Реализация

Напишем такой класс:

Запустим его с помощью следующего кода:

Получаем:

Комментарии к коду:

Как видите, код в этом примере и в прошлом почти идентичный. Единственная разница - в реализации через внутренний класс.

В чем особенность внутреннего класса? Он позволяет нам, опять же, осуществить ленивую реализацию благодаря особенностям самой Джавы. Дело в том, что внутренние классы загружаются только тогда, когда мы обращаемся к ним впервые - в отличии от "внешних" классов, которые загружаются сразу при запуске программы.

Более того, она потокобезопасна - за счет той же особенности. Почему? Есть такая проблема - если несколько потоков одновременно "стучатся" к одному и тому же объекту, один из них может получить недогруженный объект. Но тут такого нет - благодаря особенностям загрузки внутренних классов 🙂

Пример 5  - Самая хитрая реализация 🙂

Давайте попробуем реализовать его и запустить.

Реализация

Напишем такой класс:

Обратите внимание - по умолчанию конструктор у Enum приватный. Это значит, что в отличии от других реализаций, в которых мы обязательно должны указывать, что конструктор приватный (иначе Singleton-а не выйдет), то тут мы можем явно не объявлять приватный конструктор.

Запустим его с помощью следующего кода:

Получаем:

Комментарии к коду:

Теперь мы реализовали Singleton через Enum. Это очень удобно - мы можем не прописывать явно приватный конструктор (но тут мы прописали чтобы показать, как создается Singleton).

В методе main мы обратились к нему два раза. Как видите по сообщению в консоли, INSTANCE был создан всего один раз - сообщение "Singleton created! By the way, in Enums the constructor is private by default - so there is no need to write private constructor by yourself " было выведено всего один раз.


Надеемся, наша статья была Вам полезна. Можно записаться к нам на курсы по Java у нас на сайте.

Facebooktwittergoogle_plustumblrFacebooktwittergoogle_plustumblr

Facebooktwittergoogle_plustumblrFacebooktwittergoogle_plustumblr
Самоучители--узнать детальнее--