Последнее изменение: 13 июля 2010г.
Реализация шаблона Singleton
Для начала хочу сказать, что такое вообще Singleton
и когда он может быть полезен.
Это шаблон, который обеспечивает существование одного (реже более одного) экземпляра класса, без возможности прямого создания
этого класса. Соответственно, полезен он именно в тех ситуациях, где такое поведение необходимо. Скажем, в приложении может
быть только один менеджер работы с базой данных, а создание еще одного не только нецелесообразно, но и вредно. Чтобы
предотвратить саму такую возможность как раз и используется Singleton
.
По здравому размышлению я решил дать тут некоторое разъяснение. Singleton
– это вовсе не обязательно
единственный экземпляр класса. Таких экземпляров может быть несколько. Скажем, каждый элемент перечисления
public enum Digit{ zero, one, two, three, four, five, six, seven, eight, nine }
... существует в единственном экземпляре и является экземпляром класса Digit
. Никаким образом не может быть
создан второй экземпляр любого из этих объектов. Следовательно, класс перечисления Digit
соответствует шаблону
Singleton
. Точно так же ему будет соответствовать перечисление, реализованное вручную (
private
-конструктор и набор public static final
полей – элементов перечисления). Ключевая
особенность этого шаблона, таким образом, – отсутствие возможности создания экземпляров класса, кроме
предоставляемых.
Техника реализации шаблона Singleton
зависит от двух аспектов:
Ниже мы рассмотрим требования каждого из них.
Тип создаваемого объекта
С точки зрения реализации Singleton
существуют два типа объектов, которые необходимо создавать: абстрактные
(т.е. нам необходим единственный экземпляр интерфейса или абстрактного класса) и конкретные классы.
Необходимость создавать интерфейсы или абстрактные классы возникает довольно часто. Классический случай – в библиотеку
включены интерфейсы-описания объектов и закрытая реализация. Пользователь библиотеки имеет дело только с интерфейсами.
Пример такого подхода есть в Java 1.4+ – поддержка XML. Пакет javax.xml.parsers
содержит абстрактный класс
DocumentBuilder
, который в системе нужен только в одном экземпляре.
Реализация заключается в следующем. Создается вспомогательный класс – factory
(на самом деле это применение
еще одного шаблона, который так и называется – Factory
). Этот класс, как правило, не может быть создан напрямую
– например, он тоже абстрактный, или не имеет public
-конструкторов. Класс factory
создает
экземпляр нужного объекта (как именно – неважно, например, реализуя нужный интерфейс или наследуя свой внутренний класс от
нужного абстрактного класса) и возвращает его при вызове статического метода. Пример такого подхода приведен ниже. (Я прошу
не рассматривать этот код как руководство к действию, ибо в нем я намеренно не учитываю моменты, которые будут рассмотрены
далее.)
public interface SomeInterface{ public void doSomething(); } public class SomeInterfaceFactory{ private static SomeInterface impl = null; private SomeInterfaceFactory(){ } public static SomeInterface getSomeInterface(){ if (impl == null){ impl = new SomeInterfaceImpl(); } return impl; } private class SomeInterfaceImpl implements SomeInterface{ public void doSomething(){ // ... implementation } } }
Как можно видеть из этого кода, при первом обращении к SomeInterfaceFactory.getSomeInterface()
происходит
создание объекта, и именно этот объект выдается при любом последующем вызове. Очевидно, что никто не мешает создать не один
объект, а несколько, и выдавать какой-либо из них, при определенным правилам (бывает, что такое необходимо).
Этот подход хорошо работает в случае с интерфейсом или абстрактным классом. Более того, в этом случае он единственный. В случае же с конкретным классом у него имеется существенный, на мой взгляд, недостаток.
Дело в том, что объекту factory
надо каким-то образом создавать нужный класс. Это означает, что у этого
класса должен быть конструктор, доступный извне. Но! Если этот конструктор доступен кому-то еще – он доступен всем. Не
спасет даже доступ по умолчанию, т.к. никто не сможет помешать создать свой класс в необходимом пакете и вызвать конструктор,
создав тем самым новый объект.
Правда, если классы упакованы в jar-файл, то не все так плохо – нужный пакет можно объявить как закрытый (sealed), что означает, что все классы этого пакета должны быть в одном архиве. О том, как это сделать, можно прочитать вот тут: http://java.sun.com/docs/books/tutorial/deployment/jar/sealman.html. Соответственно, использовать конструктор с доступом по умолчанию фабрика сможет, а любой код, не находящийся в данном jar-файле – нет. Однако об этой возможности знают далеко не все.
Потому, в случае конкретного класса технику лучше изменить:
public class SingletonImpl{ private static SingletonImpl self = new SingletonImpl(); private SingletonImpl(){ super(); // perform initialization here } public static SingletonImpl getInstance(){ return self; } }
Как видно из приведенного кода, нужный объект создается в момент загрузки класса и дальше выдается при всех обращениях к
SingletonImpl.getInstance()
. При этом используется private
-конструктор, что делает невозможным
создание этого объекта извне.
С различиями, обусловленными разными типами классов, закончили. Переходим к следующей части.
Инициализация и синхронизация
Вернемся к примеру, рассмотренному только что. Его недостаток очевиден: объект создается всегда, вне зависимости от того, будем мы использовать его или нет. Если создание объекта требует много ресурсов – такой вариант не самый удачный.
В этом случае может спасти так называемая отложенная инициализация – создание объекта в тот момент, когда он требуется. Пример приведен ниже.
public class SingletonImpl{ private static SingletonImpl self = null; private SingletonImpl(){ super(); // perform initialization here self = this; } public static SingletonImpl getInstance(){ return (self == null) ? new SingletonImpl() : self; } }
Как видно из кода, нужный объект создается при первом обращении к SingletonImpl.getInstance()
. Конструктор
private
, так что создание объекта извне невозможно. Статическая ссылка на единственный экземпляр объекта
инициализируется при вызове конструктора, так что при последующих вызовах именно этот объект и будет выдан.
Хорошо? На самом деле, не очень. Дело вот в чем. При практически одновременном первом обращении к
SingletonImpl.getInstance()
из двух разных потоков может возникнуть такая ситуация: каждый из потоков увидит
переменную self
, имеющую значение null
. В результате каждый из них начнет создание нового
экземпляра. Хорошо, если это безболезненная процедура – тогда переменная self
будет просто ссылаться на второй
из созданных объектов, а первый будет в дальнейшем убран сборщиком мусора. Да и в этом случае возможны осложнения – если
приложение где-нибудь сохранит ссылку на получанный объект, в рассчете на то, что она неизменна, в системе будет ДВА
экземпляра вместо одного. А если инициализация блокирует какие-либо ресурсы, так что второй объект просто не будет создан?
Получится не совсем то, чего мы добивались.
Для того, чтобы избежать такой ситуации, нужно синхронизировать доступ к переменной self
. Наилучшим способом
является следующий: объявить метод getInstance
как synchronized
. В этом случае виртуальная
машина гарантирует, что в каждый момент времени только один поток имеет возможность исполнять код метода
getInstance
, и подобных коллизий возникнуть не должно. Таким образом, код превращается в следующий:
public class SingletonImpl{ private static SingletonImpl self = null; private SingletonImpl(){ super(); // perform initialization here self = this; } public static synchronized SingletonImpl getInstance(){ return (self == null) ? new SingletonImpl() : self; } }
Я бы сказал, что это – окончательный вариант реализации Singleton
в случае отложенной инициализации.
Однако этот вариант вызывает критику у некоторых разработчиков. Их аргументы – синхронизация нужна только один раз, при создании экземпляра, а накладные расходы она дает при каждом вызове.
Теоретически это верно. Правда, тут есть одно "но". Первое – современные виртуальные машины имеют крайне низкие накладные расходы, связанные с синхронизацией. А заниматься оптимизацией именно этого фрагмента, не будучи уверенным, что именно он является узким местом в производительности... Как говорил Дональд Кнут, "Преждевременная оптимизация является первопричиной всех бед в программировании".
Эти соображения, тем не менее, не находят понимания. В результате я видел следующий вариант метода getInstance():
public static SingletonImpl getInstance(){ if (self == null){ synchronized(SingletonImpl.class){ if (self == null){ self = new SingletonImpl(); } } } return self; }
Этот прием носит специальное название – Double-checked locking. Теоретически он вполне понятен, более того, когда-то он считался одним из правильных шаблонов. Проверка, потом синхронизация, потом еще одна проверка – на случай, если за это время другой поток уже успел создать объект. Практически же такой подход ничего не гарантирует. В отсутствие синхронизации на уровне метода поток может не увидеть изменения, сделаные другим потоком. Почему именно – я не буду сейчас описывать. Это означало бы просто переписать страницу из книги Джошуа Блоха. Потому всех интересующихся отсылаю к этой книге: Джошуа Блох. Java. Эффективное программирование. Нужная статья – 48. Блох рассматривает как раз этот вариант реализации отложенной инициализации.
Также все желающие могут ознакомиться вот с этой статьей с сайта IBM DeveloperWorks – Double-checked locking and the Singleton pattern (http://www.ibm.com/developerworks/java/library/j-dcl.html). В ней рассматривается вариант отложенной инициализации с двойной проверкой, причем вплоть до уровня ассемблера.
Все сказаное выше про отложенную инициализацию и синхронизацию применимо, разумеется, и в случае создания экземпляров интерфейсов или абстрактных класов.
Собственно, это всё о реализации Singleton
. Надеюсь, эта статья окажется полезной. Всем спасибо за внимание!